[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
9 changed files with 195 additions and 34 deletions

View File

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

View File

@@ -3,20 +3,26 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Controls;
using ManagedCommon; using ManagedCommon;
using Microsoft.UI; using Microsoft.UI;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Peek.Common.Constants; using Peek.Common.Constants;
using Peek.Common.Helpers;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.System; using Windows.System;
using Windows.UI; using Windows.UI;
using Control = System.Windows.Controls.Control;
namespace Peek.FilePreviewer.Controls namespace Peek.FilePreviewer.Controls
{ {
public sealed partial class BrowserControl : UserControl, IDisposable public sealed partial class BrowserControl : Microsoft.UI.Xaml.Controls.UserControl, IDisposable
{ {
/// <summary> /// <summary>
/// Helper private Uri where we cache the last navigated page /// 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() public BrowserControl()
{ {
this.InitializeComponent(); this.InitializeComponent();
@@ -78,6 +103,7 @@ namespace Peek.FilePreviewer.Controls
if (PreviewBrowser.CoreWebView2 != null) if (PreviewBrowser.CoreWebView2 != null)
{ {
PreviewBrowser.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded; 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.DefaultBackgroundColor = Color.FromArgb(0, 0, 0, 0);
PreviewBrowser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; PreviewBrowser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; PreviewBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true;
PreviewBrowser.CoreWebView2.Settings.AreDevToolsEnabled = false; PreviewBrowser.CoreWebView2.Settings.AreDevToolsEnabled = false;
PreviewBrowser.CoreWebView2.Settings.AreHostObjectsAllowed = false; PreviewBrowser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
PreviewBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false; PreviewBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
@@ -164,6 +190,7 @@ namespace Peek.FilePreviewer.Controls
PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded; PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
PreviewBrowser.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested; PreviewBrowser.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
PreviewBrowser.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -173,6 +200,87 @@ namespace Peek.FilePreviewer.Controls
Navigate(); 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) 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. // 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) 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) if (args.IsSuccess)
{ {

View File

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

View File

@@ -11,5 +11,7 @@ namespace Peek.FilePreviewer.Previewers.Interfaces
public Uri? Preview { get; } public Uri? Preview { get; }
public bool IsDevFilePreview { 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_LANG]]", vsCodeLangSet, StringComparison.InvariantCulture);
html = html.Replace("[[PT_WRAP]]", wrapText ? "1" : "0", 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_STICKY_SCROLL]]", stickyScroll ? "1" : "0", StringComparison.InvariantCulture);
html = html.Replace("[[PT_THEME]]", theme, StringComparison.InvariantCulture); html = html.Replace("[[PT_THEME]]", theme, StringComparison.InvariantCulture);
html = html.Replace("[[PT_FONT_SIZE]]", fontSize.ToString(CultureInfo.InvariantCulture), 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] [ObservableProperty]
private bool isDevFilePreview; private bool isDevFilePreview;
[ObservableProperty]
private bool customContextMenu;
private bool disposed; private bool disposed;
public WebBrowserPreviewer(IFileSystemItem file, IPreviewSettings previewSettings) public WebBrowserPreviewer(IFileSystemItem file, IPreviewSettings previewSettings)
@@ -107,9 +110,14 @@ namespace Peek.FilePreviewer.Previewers
{ {
bool isHtml = File.Extension == ".html" || File.Extension == ".htm"; bool isHtml = File.Extension == ".html" || File.Extension == ".htm";
bool isMarkdown = File.Extension == ".md"; 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()); 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)); 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> <value>Length: {0}</value>
<comment>{0} is the duration of the audio read from file metadata</comment> <comment>{0} is the duration of the audio read from file metadata</comment>
</data> </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> </root>

View File

@@ -143,7 +143,7 @@ namespace Microsoft.PowerToys.PreviewHandler.Markdown
await _browser.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true); await _browser.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true);
_browser.CoreWebView2.SetVirtualHostNameToFolderMapping(VirtualHostName, AssemblyDirectory, CoreWebView2HostResourceAccessKind.Deny); _browser.CoreWebView2.SetVirtualHostNameToFolderMapping(VirtualHostName, AssemblyDirectory, CoreWebView2HostResourceAccessKind.Deny);
_browser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false; _browser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
_browser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; _browser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true;
_browser.CoreWebView2.Settings.AreDevToolsEnabled = false; _browser.CoreWebView2.Settings.AreDevToolsEnabled = false;
_browser.CoreWebView2.Settings.AreHostObjectsAllowed = false; _browser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
_browser.CoreWebView2.Settings.IsGeneralAutofillEnabled = 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 // WebView2.NavigateToString() limitation
// See https://learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2.navigatetostring?view=webview2-dotnet-1.0.864.35#remarks // 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 // 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 = FilePreviewCommon.MonacoHelper.ReadIndexHtml();
_html = _html.Replace("[[PT_LANG]]", _vsCodeLangSet, StringComparison.InvariantCulture); _html = _html.Replace("[[PT_LANG]]", _vsCodeLangSet, StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_WRAP]]", _settings.Wrap ? "1" : "0", 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_THEME]]", Settings.GetTheme(), StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_STICKY_SCROLL]]", _settings.StickyScroll ? "1" : "0", 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); _html = _html.Replace("[[PT_FONT_SIZE]]", _settings.FontSize.ToString(CultureInfo.InvariantCulture), StringComparison.InvariantCulture);