Browse Source

Implement TopLevel.Launcher (#14320)

* Implement ILauncher

* Update dialogs page to include Launcher buttons

* Fix control catalog

* Add return comments
pull/14530/head
Max Katz 2 years ago
committed by GitHub
parent
commit
df4189ce0e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      samples/ControlCatalog/Pages/DialogsPage.xaml
  2. 31
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  3. 56
      src/Android/Avalonia.Android/Platform/AndroidLauncher.cs
  4. 7
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 90
      src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs
  6. 69
      src/Avalonia.Base/Platform/Storage/ILauncher.cs
  7. 3
      src/Avalonia.Controls/TopLevel.cs
  8. 50
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  9. 6
      src/Avalonia.Native/WindowImplBase.cs
  10. 7
      src/Avalonia.X11/X11Window.cs
  11. 3
      src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs
  12. 28
      src/Browser/Avalonia.Browser/Storage/BrowserLauncher.cs
  13. 1
      src/Tizen/Avalonia.Tizen/Platform/Permissions.cs
  14. 71
      src/Tizen/Avalonia.Tizen/Platform/TizenLauncher.cs
  15. 5
      src/Tizen/Avalonia.Tizen/TopLevelImpl.cs
  16. 8
      src/Windows/Avalonia.Win32/WindowImpl.cs
  17. 5
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  18. 45
      src/iOS/Avalonia.iOS/IOSLauncher.cs

9
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -51,6 +51,15 @@
<Button Name="OpenBoth">Select _Both</Button> <Button Name="OpenBoth">Select _Both</Button>
</StackPanel> </StackPanel>
</Expander> </Expander>
<Expander Header="Launcher dialogs">
<StackPanel Spacing="4">
<TextBox Name="UriToLaunch" Watermark="Uri to launch" Text="https://avaloniaui.net/" />
<Button Name="LaunchUri">Launch Uri</Button>
<Button Name="LaunchFile">Launch File</Button>
<TextBlock Name="LaunchStatus" />
</StackPanel>
</Expander>
<AutoCompleteBox x:Name="CurrentFolderBox" Watermark="Write full path/uri or well known folder name"> <AutoCompleteBox x:Name="CurrentFolderBox" Watermark="Write full path/uri or well known folder name">
<AutoCompleteBox.ItemsSource> <AutoCompleteBox.ItemsSource>

31
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -25,6 +25,7 @@ namespace ControlCatalog.Pages
this.InitializeComponent(); this.InitializeComponent();
IStorageFolder? lastSelectedDirectory = null; IStorageFolder? lastSelectedDirectory = null;
IStorageItem? lastSelectedItem = null;
bool ignoreTextChanged = false; bool ignoreTextChanged = false;
var results = this.Get<ItemsControl>("PickerLastResults"); var results = this.Get<ItemsControl>("PickerLastResults");
@ -290,11 +291,40 @@ namespace ControlCatalog.Pages
await SetPickerResult(folder is null ? null : new[] { folder }); await SetPickerResult(folder is null ? null : new[] { folder });
SetFolder(folder); SetFolder(folder);
}; };
this.Get<Button>("LaunchUri").Click += async delegate
{
var statusBlock = this.Get<TextBlock>("LaunchStatus");
if (Uri.TryCreate(this.Get<TextBox>("UriToLaunch").Text, UriKind.Absolute, out var uri))
{
var result = await TopLevel.GetTopLevel(this)!.Launcher.LaunchUriAsync(uri);
statusBlock.Text = "LaunchUriAsync returned " + result;
}
else
{
statusBlock.Text = "Can't parse the Uri";
}
};
this.Get<Button>("LaunchFile").Click += async delegate
{
var statusBlock = this.Get<TextBlock>("LaunchStatus");
if (lastSelectedItem is not null)
{
var result = await TopLevel.GetTopLevel(this)!.Launcher.LaunchFileAsync(lastSelectedItem);
statusBlock.Text = "LaunchFileAsync returned " + result;
}
else
{
statusBlock.Text = "Please select any file or folder first";
}
};
void SetFolder(IStorageFolder? folder) void SetFolder(IStorageFolder? folder)
{ {
ignoreTextChanged = true; ignoreTextChanged = true;
lastSelectedDirectory = folder; lastSelectedDirectory = folder;
lastSelectedItem = folder;
currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString(); currentFolderBox.Text = folder?.Path is { IsAbsoluteUri: true } abs ? abs.LocalPath : folder?.Path?.ToString();
ignoreTextChanged = false; ignoreTextChanged = false;
} }
@ -344,6 +374,7 @@ namespace ControlCatalog.Pages
} }
} }
} }
lastSelectedItem = item;
} }
results.ItemsSource = mappedResults; results.ItemsSource = mappedResults;

56
src/Android/Avalonia.Android/Platform/AndroidLauncher.cs

@ -0,0 +1,56 @@
using System;
using System.Threading.Tasks;
using Android.Content;
using Avalonia.Android.Platform.Storage;
using Avalonia.Platform.Storage;
using AndroidUri = Android.Net.Uri;
namespace Avalonia.Android.Platform;
internal class AndroidLauncher : ILauncher
{
private readonly Context _context;
public AndroidLauncher(Context context)
{
_context = context;
}
public Task<bool> LaunchUriAsync(Uri uri)
{
_ = uri ?? throw new ArgumentNullException(nameof(uri));
if (uri.IsAbsoluteUri && _context.PackageManager is { } packageManager)
{
var intent = new Intent(Intent.ActionView, AndroidUri.Parse(uri.OriginalString));
if (intent.ResolveActivity(packageManager) is not null)
{
var flags = ActivityFlags.ClearTop | ActivityFlags.NewTask;
intent.SetFlags(flags);
_context.StartActivity(intent);
}
}
return Task.FromResult(false);
}
public Task<bool> LaunchFileAsync(IStorageItem storageItem)
{
_ = storageItem ?? throw new ArgumentNullException(nameof(storageItem));
var androidUri = (storageItem as AndroidStorageItem)?.Uri
?? (storageItem.TryGetLocalPath() is { } localPath ? AndroidUri.Parse(localPath) : null);
if (androidUri is not null && _context.PackageManager is { } packageManager)
{
var intent = new Intent(Intent.ActionView, androidUri);
// intent.SetDataAndType(contentUri, request.File.ContentType);
intent.SetFlags(ActivityFlags.GrantReadUriPermission);
if (intent.ResolveActivity(packageManager) is not null
&& Intent.CreateChooser(intent, string.Empty) is { } chooserIntent)
{
var flags = ActivityFlags.ClearTop | ActivityFlags.NewTask;
chooserIntent.SetFlags(flags);
_context.StartActivity(chooserIntent);
}
}
return Task.FromResult(false);
}
}

7
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -44,6 +44,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidSystemNavigationManagerImpl _systemNavigationManager; private readonly AndroidSystemNavigationManagerImpl _systemNavigationManager;
private readonly AndroidInsetsManager _insetsManager; private readonly AndroidInsetsManager _insetsManager;
private readonly ClipboardImpl _clipboard; private readonly ClipboardImpl _clipboard;
private readonly AndroidLauncher _launcher;
private ViewImpl _view; private ViewImpl _view;
private WindowTransparencyLevel _transparencyLevel; private WindowTransparencyLevel _transparencyLevel;
@ -70,6 +71,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
_storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
_transparencyLevel = WindowTransparencyLevel.None; _transparencyLevel = WindowTransparencyLevel.None;
_launcher = new AndroidLauncher((Activity)avaloniaView.Context);
_systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService); _systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService);
@ -404,6 +406,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return _clipboard; return _clipboard;
} }
if (featureType == typeof(ILauncher))
{
return _launcher;
}
return null; return null;
} }

90
src/Avalonia.Base/Platform/Storage/FileIO/BclLauncher.cs

@ -0,0 +1,90 @@
using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Compatibility;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO;
internal class BclLauncher : ILauncher
{
public virtual Task<bool> LaunchUriAsync(Uri uri)
{
_ = uri ?? throw new ArgumentNullException(nameof(uri));
if (uri.IsAbsoluteUri)
{
return Task.FromResult(Exec(uri.AbsoluteUri));
}
return Task.FromResult(false);
}
/// <summary>
/// This Process based implementation doesn't handle the case, when there is no app to handle link.
/// It will still return true in this case.
/// </summary>
public virtual Task<bool> LaunchFileAsync(IStorageItem storageItem)
{
_ = storageItem ?? throw new ArgumentNullException(nameof(storageItem));
if (storageItem.TryGetLocalPath() is { } localPath
&& CanOpenFileOrDirectory(localPath))
{
return Task.FromResult(Exec(localPath));
}
return Task.FromResult(false);
}
protected virtual bool CanOpenFileOrDirectory(string localPath) => true;
private static bool Exec(string urlOrFile)
{
if (OperatingSystemEx.IsLinux())
{
// If no associated application/json MimeType is found xdg-open opens return error
// but it tries to open it anyway using the console editor (nano, vim, other..)
ShellExec($"xdg-open {urlOrFile}", waitForExit: false);
return true;
}
else if (OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS())
{
using var process = Process.Start(new ProcessStartInfo
{
FileName = OperatingSystemEx.IsWindows() ? urlOrFile : "open",
Arguments = OperatingSystemEx.IsMacOS() ? $"{urlOrFile}" : "",
CreateNoWindow = true,
UseShellExecute = OperatingSystemEx.IsWindows()
});
return true;
}
else
{
return false;
}
}
private static void ShellExec(string cmd, bool waitForExit = true)
{
var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\")
.Replace("\"", "\\\\\\\"");
using (var process = Process.Start(
new ProcessStartInfo
{
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
))
{
if (waitForExit)
{
process?.WaitForExit();
}
}
}
}

69
src/Avalonia.Base/Platform/Storage/ILauncher.cs

@ -0,0 +1,69 @@
using System;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Starts the default app associated with the specified file or URI.
/// </summary>
public interface ILauncher
{
/// <summary>
/// Starts the default app associated with the URI scheme name for the specified URI.
/// </summary>
/// <param name="uri">The URI.</param>
/// <returns>True, if launch operation was successful. False, if unsupported or failed.</returns>
Task<bool> LaunchUriAsync(Uri uri);
/// <summary>
/// Starts the default app associated with the specified storage file or folder.
/// </summary>
/// <param name="storageItem">The file or folder.</param>
/// <returns>True, if launch operation was successful. False, if unsupported or failed.</returns>
Task<bool> LaunchFileAsync(IStorageItem storageItem);
}
internal class NoopLauncher : ILauncher
{
public Task<bool> LaunchUriAsync(Uri uri) => Task.FromResult(false);
public Task<bool> LaunchFileAsync(IStorageItem storageItem) => Task.FromResult(false);
}
public static class LauncherExtensions
{
/// <summary>
/// Starts the default app associated with the specified storage file.
/// </summary>
/// <param name="launcher">ILauncher instance.</param>
/// <param name="fileInfo">The file.</param>
public static Task<bool> LaunchFileInfoAsync(this ILauncher launcher, FileInfo fileInfo)
{
_ = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
if (!fileInfo.Exists)
{
return Task.FromResult(false);
}
return launcher.LaunchFileAsync(new BclStorageFile(fileInfo));
}
/// <summary>
/// Starts the default app associated with the specified storage directory (folder).
/// </summary>
/// <param name="launcher">ILauncher instance.</param>
/// <param name="directoryInfo">The directory.</param>
public static Task<bool> LaunchDirectoryInfoAsync(this ILauncher launcher, DirectoryInfo directoryInfo)
{
_ = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
if (!directoryInfo.Exists)
{
return Task.FromResult(false);
}
return launcher.LaunchFileAsync(new BclStorageFolder(directoryInfo));
}
}

3
src/Avalonia.Controls/TopLevel.cs

@ -549,6 +549,7 @@ namespace Avalonia.Controls
public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>(); public IInsetsManager? InsetsManager => PlatformImpl?.TryGetFeature<IInsetsManager>();
public IInputPane? InputPane => PlatformImpl?.TryGetFeature<IInputPane>(); public IInputPane? InputPane => PlatformImpl?.TryGetFeature<IInputPane>();
public ILauncher Launcher => PlatformImpl?.TryGetFeature<ILauncher>() ?? new NoopLauncher();
/// <summary> /// <summary>
/// Gets the platform's clipboard implementation /// Gets the platform's clipboard implementation
@ -560,7 +561,7 @@ namespace Avalonia.Controls
/// <inheritdoc /> /// <inheritdoc />
public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>(); public IPlatformSettings? PlatformSettings => AvaloniaLocator.Current.GetService<IPlatformSettings>();
/// <inheritdoc/> /// <inheritdoc/>
Point IRenderRoot.PointToClient(PixelPoint p) Point IRenderRoot.PointToClient(PixelPoint p)
{ {

50
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@ -1,7 +1,4 @@
using System; using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
@ -24,51 +21,10 @@ namespace Avalonia.Dialogs
DataContext = this; DataContext = this;
} }
private async void Button_OnClick(object sender, RoutedEventArgs e)
private static void ShellExec(string cmd, bool waitForExit = true)
{
var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\")
.Replace("\"", "\\\\\\\"");
using (var process = Process.Start(
new ProcessStartInfo
{
FileName = "/bin/sh",
Arguments = $"-c \"{escapedArgs}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
))
{
if (waitForExit)
{
process?.WaitForExit();
}
}
}
private void Button_OnClick(object sender, RoutedEventArgs e)
{ {
var url = "https://www.avaloniaui.net/"; var url = new Uri("https://www.avaloniaui.net/");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) await Launcher.LaunchUriAsync(url);
{
// If no associated application/json MimeType is found xdg-open opens retrun error
// but it tries to open it anyway using the console editor (nano, vim, other..)
ShellExec($"xdg-open {url}", waitForExit: false);
}
else
{
using Process? process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
});
}
} }
} }
} }

6
src/Avalonia.Native/WindowImplBase.cs

@ -12,6 +12,7 @@ using Avalonia.Input.Raw;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Threading; using Avalonia.Threading;
@ -581,6 +582,11 @@ namespace Avalonia.Native
return AvaloniaLocator.Current.GetRequiredService<IClipboard>(); return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
} }
if (featureType == typeof(ILauncher))
{
return new BclLauncher();
}
return null; return null;
} }

7
src/Avalonia.X11/X11Window.cs

@ -24,6 +24,8 @@ using Avalonia.X11.NativeDialogs;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Platform.Storage.FileIO;
// ReSharper disable IdentifierTypo // ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo // ReSharper disable StringLiteralTypo
@ -904,6 +906,11 @@ namespace Avalonia.X11
return AvaloniaLocator.Current.GetRequiredService<IClipboard>(); return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
} }
if (featureType == typeof(ILauncher))
{
return new BclLauncher();
}
return null; return null;
} }

3
src/Browser/Avalonia.Browser/Interop/NavigationHelper.cs

@ -7,4 +7,7 @@ internal static partial class NavigationHelper
{ {
[JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)] [JSImport("NavigationHelper.addBackHandler", AvaloniaModule.MainModuleName)]
public static partial void AddBackHandler([JSMarshalAs<JSType.Function<JSType.Boolean>>] Func<bool> backHandlerCallback); public static partial void AddBackHandler([JSMarshalAs<JSType.Function<JSType.Boolean>>] Func<bool> backHandlerCallback);
[JSImport("window.open")]
public static partial JSObject? WindowOpen(string uri, string target);
} }

28
src/Browser/Avalonia.Browser/Storage/BrowserLauncher.cs

@ -0,0 +1,28 @@
using System;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
using Avalonia.Platform.Storage;
namespace Avalonia.Browser.Storage;
internal class BrowserLauncher : ILauncher
{
public Task<bool> LaunchUriAsync(Uri uri)
{
_ = uri ?? throw new ArgumentNullException(nameof(uri));
if (uri.IsAbsoluteUri)
{
var window = NavigationHelper.WindowOpen(uri.AbsoluteUri, "_blank");
return Task.FromResult(window is not null);
}
return Task.FromResult(false);
}
public Task<bool> LaunchFileAsync(IStorageItem storageItem)
{
_ = storageItem ?? throw new ArgumentNullException(nameof(storageItem));
return Task.FromResult(false);
}
}

1
src/Tizen/Avalonia.Tizen/Platform/Permissions.cs

@ -19,6 +19,7 @@ internal class Permissions
public static readonly Privilege MediaStoragePrivilege = new("http://tizen.org/privilege/mediastorage", true); public static readonly Privilege MediaStoragePrivilege = new("http://tizen.org/privilege/mediastorage", true);
public static readonly Privilege RecorderPrivilege = new("http://tizen.org/privilege/recorder", false); public static readonly Privilege RecorderPrivilege = new("http://tizen.org/privilege/recorder", false);
public static readonly Privilege HapticPrivilege = new("http://tizen.org/privilege/haptic", false); public static readonly Privilege HapticPrivilege = new("http://tizen.org/privilege/haptic", false);
public static readonly Privilege LaunchPrivilege = new("http://tizen.org/privilege/appmanager.launch", false);
public static readonly Privilege[] NetworkPrivileges = { InternetPrivilege, NetworkPrivilege }; public static readonly Privilege[] NetworkPrivileges = { InternetPrivilege, NetworkPrivilege };
public static readonly Privilege[] MapsPrivileges = { InternetPrivilege, MapServicePrivilege, NetworkPrivilege }; public static readonly Privilege[] MapsPrivileges = { InternetPrivilege, MapServicePrivilege, NetworkPrivilege };

71
src/Tizen/Avalonia.Tizen/Platform/TizenLauncher.cs

@ -0,0 +1,71 @@
using Avalonia.Platform.Storage;
using Avalonia.Tizen.Platform;
using Tizen.Applications;
namespace Avalonia.Tizen;
internal class TizenLauncher : ILauncher
{
public async Task<bool> LaunchUriAsync(Uri uri)
{
if (uri is null)
{
throw new ArgumentNullException(nameof(uri));
}
if (!uri.IsAbsoluteUri)
{
return false;
}
if (!await Permissions.RequestPrivilegeAsync(Permissions.LaunchPrivilege))
{
return false;
}
var appControl = new AppControl
{
Operation = AppControlOperations.ShareText,
Uri = uri.AbsoluteUri
};
if (uri.AbsoluteUri.StartsWith("geo:"))
appControl.Operation = AppControlOperations.Pick;
else if (uri.AbsoluteUri.StartsWith("http"))
appControl.Operation = AppControlOperations.View;
else if (uri.AbsoluteUri.StartsWith("mailto:"))
appControl.Operation = AppControlOperations.Compose;
else if (uri.AbsoluteUri.StartsWith("sms:"))
appControl.Operation = AppControlOperations.Compose;
else if (uri.AbsoluteUri.StartsWith("tel:"))
appControl.Operation = AppControlOperations.Dial;
AppControl.SendLaunchRequest(appControl);
return true;
}
public async Task<bool> LaunchFileAsync(IStorageItem storageItem)
{
if (storageItem is null)
{
throw new ArgumentNullException(nameof(storageItem));
}
if (!await Permissions.RequestPrivilegeAsync(Permissions.LaunchPrivilege))
{
return false;
}
var appControl = new AppControl
{
Operation = AppControlOperations.View,
Mime = "*/*",
Uri = "file://" + storageItem.Path,
};
AppControl.SendLaunchRequest(appControl);
return true;
}
}

5
src/Tizen/Avalonia.Tizen/TopLevelImpl.cs

@ -104,6 +104,11 @@ internal class TopLevelImpl : ITopLevelImpl
return _clipboard; return _clipboard;
} }
if (featureType == typeof(ILauncher))
{
return new TizenLauncher();
}
return null; return null;
} }

8
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -26,6 +26,7 @@ using Avalonia.Win32.WinRT;
using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.Interop.UnmanagedMethods;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Threading; using Avalonia.Threading;
using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl;
using static Avalonia.Controls.Platform.Win32SpecificOptions; using static Avalonia.Controls.Platform.Win32SpecificOptions;
@ -344,12 +345,17 @@ namespace Avalonia.Win32
{ {
return AvaloniaLocator.Current.GetRequiredService<IClipboard>(); return AvaloniaLocator.Current.GetRequiredService<IClipboard>();
} }
if (featureType == typeof(IInputPane)) if (featureType == typeof(IInputPane))
{ {
return _inputPane; return _inputPane;
} }
if (featureType == typeof(ILauncher))
{
return new BclLauncher();
}
return null; return null;
} }

5
src/iOS/Avalonia.iOS/AvaloniaView.cs

@ -287,6 +287,11 @@ namespace Avalonia.iOS
return _inputPane; return _inputPane;
} }
if (featureType == typeof(ILauncher))
{
return new IOSLauncher();
}
return null; return null;
} }
} }

45
src/iOS/Avalonia.iOS/IOSLauncher.cs

@ -0,0 +1,45 @@
using System;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Foundation;
using UIKit;
namespace Avalonia.iOS;
internal class IOSLauncher : ILauncher
{
public Task<bool> LaunchUriAsync(Uri uri)
{
_ = uri ?? throw new ArgumentNullException(nameof(uri));
if (uri.IsAbsoluteUri && UIApplication.SharedApplication.CanOpenUrl(uri))
{
return UIApplication.SharedApplication.OpenUrlAsync(uri!, new UIApplicationOpenUrlOptions());
}
return Task.FromResult(false);
}
public Task<bool> LaunchFileAsync(IStorageItem storageItem)
{
_ = storageItem ?? throw new ArgumentNullException(nameof(storageItem));
#if !TVOS
var uri = (storageItem as Storage.IOSStorageItem)?.Url
?? (storageItem.TryGetLocalPath() is { } localPath ? NSUrl.FromFilename(localPath) : null);
if (uri is not null)
{
var documentController = new UIDocumentInteractionController()
{
Name = storageItem.Name,
Url = uri
};
var result = documentController.PresentPreview(true);
return Task.FromResult(result);
}
#endif
return Task.FromResult(false);
}
}
Loading…
Cancel
Save