leave single webview control

This commit is contained in:
seraphima
2023-05-05 10:12:03 +02:00
parent d571e9f3cf
commit ea8ccede5a
14 changed files with 111 additions and 411 deletions

View File

@@ -28,9 +28,6 @@ namespace Peek.FilePreviewer.Controls
public event DOMContentLoadedHandler? DOMContentLoaded;
private string previewBrowserUserDataFolder = System.Environment.GetEnvironmentVariable("USERPROFILE") +
"\\AppData\\LocalLow\\Microsoft\\PowerToys\\Peek-Temp";
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
nameof(Source),
typeof(Uri),
@@ -43,10 +40,40 @@ namespace Peek.FilePreviewer.Controls
set { SetValue(SourceProperty, value); }
}
public static readonly DependencyProperty IsDevFilePreviewProperty = DependencyProperty.Register(
nameof(IsDevFilePreview),
typeof(bool),
typeof(BrowserControl),
new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((BrowserControl)d).SourcePropertyChanged())));
public bool IsDevFilePreview
{
get
{
return (bool)GetValue(IsDevFilePreviewProperty);
}
set
{
SetValue(IsDevFilePreviewProperty, value);
if (PreviewBrowser.CoreWebView2 != null)
{
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = value;
if (value)
{
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
}
}
}
}
public string? TempDataFolder { get; set; }
public BrowserControl()
{
this.InitializeComponent();
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", previewBrowserUserDataFolder, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", TempDataFolder, EnvironmentVariableTarget.Process);
}
public void Dispose()
@@ -68,7 +95,7 @@ namespace Peek.FilePreviewer.Controls
_navigatedUri = null;
if (Source != null)
if (Source != null && PreviewBrowser.CoreWebView2 != null)
{
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
* Use WebView2.Source to avoid re-navigating to the same content. */
@@ -93,15 +120,23 @@ namespace Peek.FilePreviewer.Controls
PreviewBrowser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
PreviewBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
PreviewBrowser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false;
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = false;
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = IsDevFilePreview;
PreviewBrowser.CoreWebView2.Settings.IsWebMessageEnabled = false;
if (IsDevFilePreview)
{
Logger.LogInfo("Set virtual host name to folder mapping: " + Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory);
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
}
PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
}
catch (Exception ex)
{
Logger.LogError("WebView2 loading failed. " + ex.Message);
}
Navigate();
}
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)

View File

@@ -1,27 +0,0 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<UserControl
x:Class="Peek.FilePreviewer.Controls.TextFilePreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
<Grid
Margin="48"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="FileInfo" Width="*" />
</Grid.ColumnDefinitions>
<controls:WebView2 Name ="PreviewText"
Loading="PreviewText_Loading"
CoreWebView2Initialized="PreviewText_CoreWebView2Initialized"
NavigationStarting="PreviewText_NavigationStarting"
NavigationCompleted="PreviewText_NavigationCompleted" />
</Grid>
</UserControl>

View File

@@ -1,164 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Peek.Common.Helpers;
using Windows.System;
namespace Peek.FilePreviewer.Controls
{
[INotifyPropertyChanged]
public sealed partial class TextFilePreview : UserControl, IDisposable
{
/// <summary>
/// Helper private Uri where we cache the last navigated page
/// so we can redirect internal PDF or Webpage links to external
/// webbrowser, avoiding WebView internal navigation.
/// </summary>
private Uri? _navigatedUri;
public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args);
public delegate void DOMContentLoadedHandler(CoreWebView2? sender, CoreWebView2DOMContentLoadedEventArgs? args);
public event NavigationCompletedHandler? NavigationCompleted;
public event DOMContentLoadedHandler? DOMContentLoaded;
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
nameof(Source),
typeof(Uri),
typeof(TextFilePreview),
new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((TextFilePreview)d).SourcePropertyChanged())));
public string? TempDataFolder { get; set; }
public Uri? Source
{
get { return (Uri)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public TextFilePreview()
{
this.InitializeComponent();
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", TempDataFolder, EnvironmentVariableTarget.Process);
}
public void Dispose()
{
if (PreviewText.CoreWebView2 != null)
{
PreviewText.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
}
}
/// <summary>
/// Navigate to the to the <see cref="Uri"/> set in <see cref="Source"/>.
/// Calling <see cref="Navigate"/> will always trigger a navigation/refresh
/// even if web target file is the same.
/// </summary>
public void Navigate()
{
var value = Environment.GetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS");
_navigatedUri = null;
if (Source != null && PreviewText.CoreWebView2 != null)
{
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
* Use WebView2.Source to avoid re-navigating to the same content. */
PreviewText.CoreWebView2.Navigate(Source.ToString());
}
}
private void SourcePropertyChanged()
{
Navigate();
}
private async void PreviewText_Loading(FrameworkElement sender, object args)
{
if (PreviewText.CoreWebView2 == null)
{
try
{
await PreviewText.EnsureCoreWebView2Async();
}
catch (Exception ex)
{
Logger.LogError("CoreWebView2 initialization failed. " + ex.Message);
}
}
}
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
{
DOMContentLoaded?.Invoke(sender, args);
}
private async void PreviewText_NavigationStarting(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args)
{
if (_navigatedUri == null)
{
return;
}
// In case user starts or tries to navigate from within the HTML file we launch default web browser for navigation.
if (args.Uri != null && args.Uri != _navigatedUri?.ToString() && args.IsUserInitiated)
{
args.Cancel = true;
await Launcher.LaunchUriAsync(new Uri(args.Uri));
}
}
private void PreviewText_NavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args)
{
if (args.IsSuccess)
{
_navigatedUri = Source;
}
// OperationCanceled status code is used when the app cancels a navigation via NavigationStarting event
if (args.WebErrorStatus != CoreWebView2WebErrorStatus.OperationCanceled)
{
NavigationCompleted?.Invoke(sender, args);
}
}
private void PreviewText_CoreWebView2Initialized(WebView2 sender, CoreWebView2InitializedEventArgs args)
{
try
{
sender.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
sender.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
sender.CoreWebView2.Settings.AreDevToolsEnabled = false;
sender.CoreWebView2.Settings.AreHostObjectsAllowed = false;
sender.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
sender.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false;
sender.CoreWebView2.Settings.IsScriptEnabled = true; // TODO: enable only for dev files
sender.CoreWebView2.Settings.IsWebMessageEnabled = false;
sender.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
sender.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
if (Source != null)
{
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
* Use WebView2.Source to avoid re-navigating to the same content. */
sender.CoreWebView2.Navigate(Source.ToString());
}
}
catch (Exception ex)
{
Logger.LogError("WebView2 loading failed. " + ex.Message);
}
}
}
}

View File

@@ -32,17 +32,10 @@
DOMContentLoaded="BrowserPreview_DOMContentLoaded"
NavigationCompleted="PreviewBrowser_NavigationCompleted"
Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}"
TempDataFolder="{x:Bind BrowserPreviewer.TempDataFolder, Mode=OneTime}"
IsDevFilePreview="{x:Bind BrowserPreviewer.IsDevFilePreview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(BrowserPreviewer, Previewer.State), Mode=OneWay, FallbackValue=Collapsed}" />
<controls:TextFilePreview
x:Name="TextPreview"
x:Load="{x:Bind IsWebPreview(TextFilePreviewer, Previewer.State), Mode=OneWay}"
Source="{x:Bind TextFilePreviewer.Preview, Mode=OneWay}"
TempDataFolder="{x:Bind TextFilePreviewer.TempDataFolder, Mode=OneTime}"
DOMContentLoaded="TextPreview_DOMContentLoaded"
NavigationCompleted="TextPreview_NavigationCompleted"
Visibility="{x:Bind IsPreviewVisible(TextFilePreviewer, Previewer.State), Mode=OneWay}" />
<controls:UnsupportedFilePreview
x:Name="UnsupportedFilePreview"
DateModified="{x:Bind UnsupportedFilePreviewer.DateModified, Mode=OneWay}"

View File

@@ -48,7 +48,6 @@ namespace Peek.FilePreviewer
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
[NotifyPropertyChangedFor(nameof(BrowserPreviewer))]
[NotifyPropertyChangedFor(nameof(TextFilePreviewer))]
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
private IPreviewer? previewer;
@@ -89,8 +88,6 @@ namespace Peek.FilePreviewer
public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer;
public ITextFilePreviewer? TextFilePreviewer => Previewer as ITextFilePreviewer;
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
public IFileSystemItem Item
@@ -126,7 +123,7 @@ namespace Peek.FilePreviewer
public bool IsWebPreview(IPreviewer? previewer, PreviewState? state)
{
var isWebViewPreview = previewer != null && (previewer is ITextFilePreviewer || previewer is IBrowserPreviewer);
var isWebViewPreview = previewer != null && previewer is IBrowserPreviewer;
return isWebViewPreview && MatchPreviewState(state, PreviewState.Loaded);
}
@@ -223,25 +220,6 @@ namespace Peek.FilePreviewer
}
}
private void TextPreview_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
{
/*
* There is an odd behavior where the WebView2 would not raise the NavigationCompleted event
* for certain HTML files, even though it has already been loaded. Probably related to certain
* extra module that require more time to load. One example is saving and opening google.com locally.
*
* So to address this, we will make the Browser visible and display it as "Loaded" as soon the HTML document
* has been parsed and loaded with the DOMContentLoaded event.
*
* Similar issue: https://github.com/MicrosoftEdge/WebView2Feedback/issues/998
*/
if (TextFilePreviewer != null)
{
TextFilePreviewer.State = PreviewState.Loaded;
}
}
private void PreviewBrowser_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
/*
@@ -261,25 +239,6 @@ namespace Peek.FilePreviewer
}
}
private void TextPreview_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
/*
* In theory most of navigation should work after DOM is loaded.
* But in case something fails we check NavigationCompleted event
* for failure and switch visibility accordingly.
*
* As an alternative, in the future, the preview control
* could also display error content.
*/
if (!args.IsSuccess)
{
if (TextFilePreviewer != null)
{
TextFilePreviewer.State = PreviewState.Error;
}
}
}
private async void KeyboardAccelerator_CtrlC_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
{
if (Previewer != null)

View File

@@ -10,7 +10,6 @@
</PropertyGroup>
<ItemGroup>
<None Remove="Controls\BrowserControl.xaml" />
<None Remove="Controls\TextFilePreview.xaml" />
<None Remove="FilePreview.xaml" />
<None Remove="UnsupportedFilePreview.xaml" />
</ItemGroup>
@@ -51,10 +50,4 @@
<ItemGroup>
<Folder Include="Previewers\DrivePreviewer\" />
</ItemGroup>
<ItemGroup>
<Page Update="Controls\TextFilePreview.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -9,5 +9,9 @@ namespace Peek.FilePreviewer.Previewers
public interface IBrowserPreviewer : IPreviewer
{
public Uri? Preview { get; }
public string TempDataFolder { get; }
public bool IsDevFilePreview { get; }
}
}

View File

@@ -1,15 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Peek.FilePreviewer.Previewers
{
public interface ITextFilePreviewer : IPreviewer
{
public Uri? Preview { get; }
public string TempDataFolder { get; }
}
}

View File

@@ -20,10 +20,6 @@ namespace Peek.FilePreviewer.Previewers
{
return new WebBrowserPreviewer(file);
}
else if (TextFilePreviewer.IsFileTypeSupported(file.Extension))
{
return new TextFilePreviewer(file);
}
// Other previewer types check their supported file types here
return CreateDefaultPreviewer(file);

View File

@@ -1,131 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Dispatching;
using Peek.Common.Extensions;
using Peek.Common.Helpers;
using Peek.Common.Models;
using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public partial class TextFilePreviewer : ObservableObject, ITextFilePreviewer, IDisposable
{
[ObservableProperty]
private Uri? preview;
[ObservableProperty]
private PreviewState state;
[ObservableProperty]
private string tempDataFolder = Environment.GetEnvironmentVariable("USERPROFILE") +
"\\AppData\\LocalLow\\Microsoft\\PowerToys\\Peek-Temp";
public TextFilePreviewer(IFileSystemItem file)
{
Item = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public bool IsPreviewLoaded => preview != null;
private IFileSystemItem Item { get; }
private DispatcherQueue Dispatcher { get; }
private Task<bool>? DisplayInfoTask { get; set; }
public void Dispose()
{
Microsoft.PowerToys.FilePreviewCommon.Helper.CleanupTempDir(tempDataFolder);
GC.SuppressFinalize(this);
}
public static bool IsFileTypeSupported(string fileExt)
{
return _supportedFileTypes.Contains(fileExt) || IsDevFile(fileExt);
}
public static bool IsDevFile(string fileExt)
{
return _supportedMonacoFileTypes.Contains(fileExt);
}
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
Size? size = new Size(1640, 1460);
return Task.FromResult(size);
}
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
State = PreviewState.Loading;
await LoadDisplayInfoAsync(cancellationToken);
if (HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
public async Task CopyAsync()
{
await Dispatcher.RunOnUiThread(async () =>
{
var storageItem = await Item.GetStorageItemAsync();
ClipboardHelper.SaveToClipboard(storageItem);
});
}
public Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
var raw = await ReadHelper.Read(Item.Path.ToString());
if (IsDevFile(Item.Extension))
{
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, Item.Extension, tempDataFolder));
}
else
{
Preview = new Uri(WebViewHelper.PreviewTempFile(raw, Item.Path, tempDataFolder));
}
});
});
}
partial void OnPreviewChanged(Uri? value)
{
if (Preview != null)
{
State = PreviewState.Loaded;
}
}
private bool HasFailedLoadingPreview()
{
var hasFailedLoadingDisplayInfo = !(DisplayInfoTask?.Result ?? true);
return hasFailedLoadingDisplayInfo;
}
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
{
".txt",
".md",
};
private static readonly HashSet<string> _supportedMonacoFileTypes = MonacoHelper.GetExtensions();
}
}

View File

@@ -8,7 +8,7 @@ using Common.UI;
namespace Peek.FilePreviewer.Previewers
{
public class WebViewHelper
public class MarkdownHelper
{
/// <summary>
/// Prepares temp html for the previewing

View File

@@ -15,7 +15,7 @@ using Windows.Foundation;
namespace Peek.FilePreviewer.Previewers
{
public partial class WebBrowserPreviewer : ObservableObject, IBrowserPreviewer
public partial class WebBrowserPreviewer : ObservableObject, IBrowserPreviewer, IDisposable
{
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
{
@@ -25,37 +25,89 @@ namespace Peek.FilePreviewer.Previewers
// Document
".pdf",
// Markdown
".md",
};
private static readonly HashSet<string> _supportedMonacoFileTypes = MonacoHelper.GetExtensions();
[ObservableProperty]
private Uri? preview;
[ObservableProperty]
private PreviewState state;
[ObservableProperty]
private string tempDataFolder = Environment.GetEnvironmentVariable("USERPROFILE") +
"\\AppData\\LocalLow\\Microsoft\\PowerToys\\Peek-Temp";
[ObservableProperty]
private bool isDevFilePreview;
public WebBrowserPreviewer(IFileSystemItem file)
{
File = file;
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
public void Dispose()
{
Microsoft.PowerToys.FilePreviewCommon.Helper.CleanupTempDir(tempDataFolder);
GC.SuppressFinalize(this);
}
private IFileSystemItem File { get; }
public bool IsPreviewLoaded => preview != null;
private DispatcherQueue Dispatcher { get; }
private Task<bool>? DisplayInfoTask { get; set; }
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
{
Size? size = null;
return Task.FromResult(size);
}
public Task LoadPreviewAsync(CancellationToken cancellationToken)
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
State = PreviewState.Loading;
await LoadDisplayInfoAsync(cancellationToken);
Preview = new Uri(File.Path);
if (HasFailedLoadingPreview())
{
State = PreviewState.Error;
}
}
return Task.CompletedTask;
public Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
{
return TaskExtension.RunSafe(async () =>
{
cancellationToken.ThrowIfCancellationRequested();
await Dispatcher.RunOnUiThread(async () =>
{
IsDevFilePreview = _supportedMonacoFileTypes.Contains(File.Extension);
if (IsDevFilePreview)
{
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, tempDataFolder));
}
else if (File.Extension == ".md")
{
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, tempDataFolder));
}
else
{
Preview = new Uri(File.Path);
}
});
});
}
public async Task CopyAsync()
@@ -69,7 +121,12 @@ namespace Peek.FilePreviewer.Previewers
public static bool IsFileTypeSupported(string fileExt)
{
return _supportedFileTypes.Contains(fileExt);
return _supportedFileTypes.Contains(fileExt) || _supportedMonacoFileTypes.Contains(fileExt);
}
private bool HasFailedLoadingPreview()
{
return !(DisplayInfoTask?.Result ?? true);
}
}
}