[Peek][PreviewPane]Show Copy entry in right-click copy menu (#33845)

## Summary of the Pull Request
Fixes two bugs:
- Peek: Missing "Copy" menu-item for all WebView2 previewers.
- PreviewPane: Missing "Copy" menu-item for markdown files only.

## Detailed Description of the Pull Request / Additional comments
The issues are:
- Peek: 
- When not using Monaco (markdown, html) - the default WebView2 context
menu has been disabled. I have enabled it and then disabled ALL
menu-items other than "Copy" (such as "Back").
- When using Monaco + Release (other code files) - current code tries to
use the Monaco context menu, but it is somehow disabled at runtime. I
spent MANY hours trying to find out why but without success. It works
fine when I view the generated html + js files in a browser or in a
Debug build or in PreviewPane. But I couldn't find the root cause.
Trying to fix it by enabling the WebView2 context menu instead doesn't
work as for whatever reason, WebView2 doesn't generate a "Copy"
menu-item (it thinks there's no selected text when there is). So in this
case, the only thing I could get to work was generating context
menu-items via WebView2 callbacks that call JS functions. As a bonus,
this way of doing it also allows "Toggle text wrapping" to work.
- PreviewPane:
- Markdown - the default WebView2 context menu has been disabled. Like
for Peek, I have enabled it and then disabled ALL menu-items other than
"Copy" (such as "Back").
- Monaco (other code files) - this already just works fine, so I've left
it as is. I *could* make it work the same way as I've done for Peek for
consistency, but I've chosen to leave it as is since it works.
  

![image](https://github.com/user-attachments/assets/d758ada7-bb62-4f40-bef7-ad08ffb83786)

![image](https://github.com/user-attachments/assets/4e0baa7e-632f-412a-b2b1-b9f666277ca7)
This commit is contained in:
Ani 2024-07-25 14:30:52 +02:00 committed by GitHub
parent ac14ad3458
commit 84def18ed5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 195 additions and 34 deletions

View File

@ -9,7 +9,7 @@
// `theme` can be "vs" for light theme or "vs-dark" for dark theme
// `lang` is the language of the file
// `wrap` if the editor is wrapping or not
var theme = ("[[PT_THEME]]" == "dark") ? "vs-dark" : "vs";
var lang = "[[PT_LANG]]";
var wrap = ([[PT_WRAP]] == 1) ? true : false;
@ -19,11 +19,29 @@
var stickyScroll = ([[PT_STICKY_SCROLL]] == 1) ? true : false;
var fontSize = [[PT_FONT_SIZE]];
var contextMenu = ([[PT_CONTEXTMENU]] == 1) ? true : false;
var editor;
// Code taken from https://stackoverflow.com/a/30106551/14774889
var code = decodeURIComponent(atob(base64code).split('').map(function(c) {
var code = decodeURIComponent(atob(base64code).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
function runToggleTextWrapCommand() {
if (wrap) {
editor.updateOptions({ wordWrap: 'off' })
} else {
editor.updateOptions({ wordWrap: 'on' })
}
wrap = !wrap;
}
function runCopyCommand() {
editor.focus();
document.execCommand('copy');
}
</script>
<!-- Set browser to Edge-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
@ -33,32 +51,33 @@
<title>Previewer for developer Files</title>
<style>
/* Fits content to window size */
html, body{
padding:0;
html, body {
padding: 0;
}
#container,.monaco-editor {
position:fixed;
height:100%;
left:0;
top:0;
right:0;
bottom:0;
#container, .monaco-editor {
position: fixed;
height: 100%;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.overflowingContentWidgets{
.overflowingContentWidgets {
/*Hides alert box */
display:none!important
}
display: none !important
}
</style>
</head>
<body oncontextmenu="onContextMenu()">
<body>
<!-- Container for the editor -->
<div id="container"></div>
<!-- Script -->
<script src="http://[[PT_URL]]/monacoSRC/min/vs/loader.js"></script>
<script src="http://[[PT_URL]]/monacoSpecialLanguages.js" type="module"></script>
<script type="module">
var editor;
<script type="module">
import { registerAdditionalLanguages } from 'http://[[PT_URL]]/monacoSpecialLanguages.js';
import { customTokenColors } from 'http://[[PT_URL]]/customTokenColors.js';
require.config({ paths: { vs: 'http://[[PT_URL]]/monacoSRC/min/vs' } });
@ -80,8 +99,9 @@
language: lang, // Sets language of the code
readOnly: true, // Sets to readonly
theme: 'theme', // Sets editor theme
minimap: {enabled: false}, // Disables minimap
minimap: { enabled: false }, // Disables minimap
lineNumbersMinChars: '3', // Width of the line numbers
contextmenu: contextMenu,
scrollbar: {
// Deactivate shadows
shadows: false,
@ -90,7 +110,7 @@
vertical: 'auto',
horizontal: 'auto',
},
stickyScroll: {enabled: stickyScroll},
stickyScroll: { enabled: stickyScroll },
fontSize: fontSize,
wordWrap: (wrap ? 'on' : 'off') // Word wraps
});
@ -117,12 +137,7 @@
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: function (ed) {
if (wrap) {
editor.updateOptions({ wordWrap: 'off' })
} else {
editor.updateOptions({ wordWrap: 'on' })
}
wrap = !wrap;
runToggleTextWrapCommand();
}
});
@ -151,4 +166,4 @@
}
</script>
</body>
</html>
</html>

View File

@ -3,20 +3,26 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Controls;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Peek.Common.Constants;
using Peek.Common.Helpers;
using Windows.ApplicationModel.DataTransfer;
using Windows.System;
using Windows.UI;
using Control = System.Windows.Controls.Control;
namespace Peek.FilePreviewer.Controls
{
public sealed partial class BrowserControl : UserControl, IDisposable
public sealed partial class BrowserControl : Microsoft.UI.Xaml.Controls.UserControl, IDisposable
{
/// <summary>
/// Helper private Uri where we cache the last navigated page
@ -67,6 +73,25 @@ namespace Peek.FilePreviewer.Controls
}
}
public static readonly DependencyProperty CustomContextMenuProperty = DependencyProperty.Register(
nameof(CustomContextMenu),
typeof(bool),
typeof(BrowserControl),
null);
public bool CustomContextMenu
{
get
{
return (bool)GetValue(CustomContextMenuProperty);
}
set
{
SetValue(CustomContextMenuProperty, value);
}
}
public BrowserControl()
{
this.InitializeComponent();
@ -78,6 +103,7 @@ namespace Peek.FilePreviewer.Controls
if (PreviewBrowser.CoreWebView2 != null)
{
PreviewBrowser.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
PreviewBrowser.CoreWebView2.ContextMenuRequested -= CoreWebView2_ContextMenuRequested;
}
}
@ -145,7 +171,7 @@ namespace Peek.FilePreviewer.Controls
PreviewBrowser.DefaultBackgroundColor = Color.FromArgb(0, 0, 0, 0);
PreviewBrowser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true;
PreviewBrowser.CoreWebView2.Settings.AreDevToolsEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
PreviewBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
@ -164,6 +190,7 @@ namespace Peek.FilePreviewer.Controls
PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
PreviewBrowser.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
PreviewBrowser.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
}
catch (Exception ex)
{
@ -173,6 +200,87 @@ namespace Peek.FilePreviewer.Controls
Navigate();
}
private List<Control> GetContextMenuItems(CoreWebView2 sender, CoreWebView2ContextMenuRequestedEventArgs args)
{
var menuItems = args.MenuItems;
if (menuItems.IsReadOnly)
{
return [];
}
if (CustomContextMenu)
{
MenuItem CreateCommandMenuItem(string resourceId, string commandName)
{
MenuItem commandMenuItem = new()
{
Header = ResourceLoaderInstance.ResourceLoader.GetString(resourceId),
IsEnabled = true,
};
commandMenuItem.Click += async (s, ex) =>
{
await sender.ExecuteScriptAsync($"{commandName}()");
};
return commandMenuItem;
}
// When using Monaco, we show menu items that call the appropriate JS functions -
// WebView2 isn't able to show a "Copy" menu item of its own.
return [
CreateCommandMenuItem("ContextMenu_Copy", "runCopyCommand"),
new Separator(),
CreateCommandMenuItem("ContextMenu_ToggleTextWrapping", "runToggleTextWrapCommand"),
];
}
else
{
MenuItem CreateMenuItemFromWebViewMenuItem(CoreWebView2ContextMenuItem webViewMenuItem)
{
MenuItem menuItem = new()
{
Header = webViewMenuItem.Label.Replace('&', '_'), // replace with '_' so it is underlined in the label
IsEnabled = webViewMenuItem.IsEnabled,
InputGestureText = webViewMenuItem.ShortcutKeyDescription,
};
menuItem.Click += (_, _) =>
{
args.SelectedCommandId = webViewMenuItem.CommandId;
};
return menuItem;
}
// When not using Monaco, we keep the "Copy" menu item from WebView2's default context menu.
return menuItems.Where(menuItem => menuItem.Name == "copy")
.Select(CreateMenuItemFromWebViewMenuItem)
.ToList<Control>();
}
}
private void CoreWebView2_ContextMenuRequested(CoreWebView2 sender, CoreWebView2ContextMenuRequestedEventArgs args)
{
var deferral = args.GetDeferral();
args.Handled = true;
var menuItems = GetContextMenuItems(sender, args);
if (menuItems.Count != 0)
{
var contextMenu = new ContextMenu();
contextMenu.Closed += (_, _) => deferral.Complete();
contextMenu.IsOpen = true;
foreach (var menuItem in menuItems)
{
contextMenu.Items.Add(menuItem);
}
}
}
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
{
// If the file being previewed is HTML or HTM, reset the background color to its original state.
@ -202,7 +310,7 @@ namespace Peek.FilePreviewer.Controls
}
}
private async void PreviewBrowser_NavigationStarting(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args)
private async void PreviewBrowser_NavigationStarting(WebView2 sender, CoreWebView2NavigationStartingEventArgs args)
{
if (_navigatedUri == null)
{
@ -218,7 +326,7 @@ namespace Peek.FilePreviewer.Controls
}
}
private void PreviewWV2_NavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args)
private void PreviewWV2_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
if (args.IsSuccess)
{

View File

@ -62,6 +62,7 @@
<controls:BrowserControl
x:Name="BrowserPreview"
x:Load="True"
CustomContextMenu="{x:Bind BrowserPreviewer.CustomContextMenu, Mode=OneWay}"
DOMContentLoaded="BrowserPreview_DOMContentLoaded"
FlowDirection="LeftToRight"
IsDevFilePreview="{x:Bind BrowserPreviewer.IsDevFilePreview, Mode=OneWay}"

View File

@ -11,5 +11,7 @@ namespace Peek.FilePreviewer.Previewers.Interfaces
public Uri? Preview { get; }
public bool IsDevFilePreview { get; }
public bool CustomContextMenu { get; }
}
}

View File

@ -80,6 +80,7 @@ namespace Peek.FilePreviewer.Previewers
html = html.Replace("[[PT_LANG]]", vsCodeLangSet, StringComparison.InvariantCulture);
html = html.Replace("[[PT_WRAP]]", wrapText ? "1" : "0", StringComparison.InvariantCulture);
html = html.Replace("[[PT_CONTEXTMENU]]", "0", StringComparison.InvariantCulture);
html = html.Replace("[[PT_STICKY_SCROLL]]", stickyScroll ? "1" : "0", StringComparison.InvariantCulture);
html = html.Replace("[[PT_THEME]]", theme, StringComparison.InvariantCulture);
html = html.Replace("[[PT_FONT_SIZE]]", fontSize.ToString(CultureInfo.InvariantCulture), StringComparison.InvariantCulture);

View File

@ -43,6 +43,9 @@ namespace Peek.FilePreviewer.Previewers
[ObservableProperty]
private bool isDevFilePreview;
[ObservableProperty]
private bool customContextMenu;
private bool disposed;
public WebBrowserPreviewer(IFileSystemItem file, IPreviewSettings previewSettings)
@ -107,9 +110,14 @@ namespace Peek.FilePreviewer.Previewers
{
bool isHtml = File.Extension == ".html" || File.Extension == ".htm";
bool isMarkdown = File.Extension == ".md";
IsDevFilePreview = MonacoHelper.SupportedMonacoFileTypes.Contains(File.Extension);
if (IsDevFilePreview && !isHtml && !isMarkdown)
bool supportedByMonaco = MonacoHelper.SupportedMonacoFileTypes.Contains(File.Extension);
bool useMonaco = supportedByMonaco && !isHtml && !isMarkdown;
IsDevFilePreview = supportedByMonaco;
CustomContextMenu = useMonaco;
if (useMonaco)
{
var raw = await ReadHelper.Read(File.Path.ToString());
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText, _previewSettings.SourceCodeStickyScroll, _previewSettings.SourceCodeFontSize));

View File

@ -318,4 +318,12 @@
<value>Length: {0}</value>
<comment>{0} is the duration of the audio read from file metadata</comment>
</data>
<data name="ContextMenu_Copy" xml:space="preserve">
<value>Copy</value>
<comment>Copy selected text to clipboard</comment>
</data>
<data name="ContextMenu_ToggleTextWrapping" xml:space="preserve">
<value>Toggle text wrapping</value>
<comment>Toggle whether text in pane is word-wrapped</comment>
</data>
</root>

View File

@ -143,7 +143,7 @@ namespace Microsoft.PowerToys.PreviewHandler.Markdown
await _browser.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true);
_browser.CoreWebView2.SetVirtualHostNameToFolderMapping(VirtualHostName, AssemblyDirectory, CoreWebView2HostResourceAccessKind.Deny);
_browser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
_browser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
_browser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true;
_browser.CoreWebView2.Settings.AreDevToolsEnabled = false;
_browser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
_browser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
@ -162,6 +162,23 @@ namespace Microsoft.PowerToys.PreviewHandler.Markdown
}
};
_browser.CoreWebView2.ContextMenuRequested += (object sender, CoreWebView2ContextMenuRequestedEventArgs args) =>
{
var menuItems = args.MenuItems;
if (!menuItems.IsReadOnly)
{
var copyMenuItem = menuItems.FirstOrDefault(menuItem => menuItem.Name == "copy");
menuItems.Clear();
if (copyMenuItem != null)
{
menuItems.Add(copyMenuItem);
}
}
};
// WebView2.NavigateToString() limitation
// See https://learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2.navigatetostring?view=webview2-dotnet-1.0.864.35#remarks
// While testing the limit, it turned out it is ~1.5MB, so to be on a safe side we go for 1.5m bytes

View File

@ -396,6 +396,7 @@ namespace Microsoft.PowerToys.PreviewHandler.Monaco
_html = FilePreviewCommon.MonacoHelper.ReadIndexHtml();
_html = _html.Replace("[[PT_LANG]]", _vsCodeLangSet, StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_WRAP]]", _settings.Wrap ? "1" : "0", StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_CONTEXTMENU]]", "1", StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_THEME]]", Settings.GetTheme(), StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_STICKY_SCROLL]]", _settings.StickyScroll ? "1" : "0", StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_FONT_SIZE]]", _settings.FontSize.ToString(CultureInfo.InvariantCulture), StringComparison.InvariantCulture);