Files
PowerToys/src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandlerControl.cs
Aaron Junker-Wildi d9008186cf [Monaco]Add option to set font size (#32559)
* Add setting to enable/disable it and add Peek support

* Add ability to set font size
2024-04-24 13:17:29 +01:00

460 lines
18 KiB
C#

// 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.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using Common;
using ManagedCommon;
using Microsoft.PowerToys.PreviewHandler.Monaco.Properties;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
using UtfUnknown;
using Windows.System;
namespace Microsoft.PowerToys.PreviewHandler.Monaco
{
public class MonacoPreviewHandlerControl : FormHandlerControl
{
/// <summary>
/// Settings class
/// </summary>
private readonly Settings _settings = new Settings();
/// <summary>
/// Text box to display the information about blocked elements from Svg.
/// </summary>
private RichTextBox _textBox;
/// <summary>
/// Represent if an text box info bar is added for showing message.
/// </summary>
private bool _infoBarAdded;
/// <summary>
/// Saves if the user already navigated to the index page
/// </summary>
private bool _hasNavigated;
/// <summary>
/// WebView2 element
/// </summary>
private WebView2 _webView;
/// <summary>
/// WebView2 Environment
/// </summary>
private CoreWebView2Environment _webView2Environment;
/// <summary>
/// Loading label
/// </summary>
private Label _loading;
/// <summary>
/// Loading progress bar
/// </summary>
private ProgressBar _loadingBar;
/// <summary>
/// Grey background
/// </summary>
private Label _loadingBackground;
/// <summary>
/// HTML code passed to the file
/// </summary>
#nullable enable
private string? _html;
#nullable disable
/// <summary>
/// Id for monaco language
/// </summary>
private string _vsCodeLangSet;
/// <summary>
/// The content of the previewing file in base64
/// </summary>
private string _base64FileCode;
public MonacoPreviewHandlerControl()
{
this.SetBackground();
}
[STAThread]
public override void DoPreview<T>(T dataSource)
{
Logger.LogTrace();
if (global::PowerToys.GPOWrapper.GPOWrapper.GetConfiguredMonacoPreviewEnabledValue() == global::PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
// GPO is disabling this utility. Show an error message instead.
_infoBarAdded = true;
AddTextBoxControl(Properties.Resources.GpoDisabledErrorText);
Resize += FormResized;
base.DoPreview(dataSource);
return;
}
base.DoPreview(dataSource);
// Starts loading screen
InitializeLoadingScreen();
// New webview2 element
_webView = new WebView2();
_webView.DefaultBackgroundColor = Color.Transparent;
try
{
// Checks if dataSource is a string
if (!(dataSource is string filePath))
{
throw new ArgumentException($"{nameof(dataSource)} for {nameof(MonacoPreviewHandlerControl)} must be a string but was a '{typeof(T)}'");
}
// Check if the file is too big.
long fileSize = new FileInfo(filePath).Length;
if (fileSize < _settings.MaxFileSize)
{
InitializeIndexFileAndSelectedFile(filePath);
Logger.LogInfo("Create WebView2 environment");
ConfiguredTaskAwaitable<CoreWebView2Environment>.ConfiguredTaskAwaiter
webView2EnvironmentAwaiter = CoreWebView2Environment
.CreateAsync(userDataFolder: System.Environment.GetEnvironmentVariable("USERPROFILE") +
"\\AppData\\LocalLow\\Microsoft\\PowerToys\\MonacoPreview-Temp")
.ConfigureAwait(true).GetAwaiter();
webView2EnvironmentAwaiter.OnCompleted(async () =>
{
_loadingBar.Value = 60;
this.Update();
try
{
if (CoreWebView2Environment.GetAvailableBrowserVersionString() == null)
{
throw new WebView2RuntimeNotFoundException();
}
_webView2Environment = webView2EnvironmentAwaiter.GetResult();
_loadingBar.Value = 70;
this.Update();
// Initialize WebView
try
{
await _webView.EnsureCoreWebView2Async(_webView2Environment).ConfigureAwait(true);
_webView.CoreWebView2.SetVirtualHostNameToFolderMapping(FilePreviewCommon.MonacoHelper.VirtualHostName, FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
Logger.LogInfo("Navigates to string of HTML file");
_webView.NavigateToString(_html);
_webView.NavigationCompleted += WebView2Init;
_webView.Height = this.Height;
_webView.Width = this.Width;
_webView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;
Controls.Add(_webView);
_webView.SendToBack();
_loadingBar.Value = 100;
this.Update();
}
catch (NullReferenceException e)
{
Logger.LogError("NullReferenceException caught. Skipping exception.", e);
}
}
catch (WebView2RuntimeNotFoundException e)
{
Logger.LogWarning("WebView2 was not found:");
Logger.LogWarning(e.Message);
Controls.Remove(_loading);
Controls.Remove(_loadingBar);
Controls.Remove(_loadingBackground);
// WebView2 not installed message
Label errorMessage = new Label();
errorMessage.Text = Resources.WebView2_Not_Installed_Message;
errorMessage.Width = TextRenderer.MeasureText(Resources.WebView2_Not_Installed_Message, errorMessage.Font).Width + 10;
errorMessage.Height = TextRenderer.MeasureText(Resources.WebView2_Not_Installed_Message, errorMessage.Font).Height;
Controls.Add(errorMessage);
// Download Link
Label downloadLink = new LinkLabel();
downloadLink.Text = Resources.Download_WebView2;
downloadLink.Click += DownloadLink_Click;
downloadLink.Top = TextRenderer.MeasureText(Resources.WebView2_Not_Installed_Message, errorMessage.Font).Height + 10;
downloadLink.Width = TextRenderer.MeasureText(Resources.Download_WebView2, errorMessage.Font).Width + 10;
downloadLink.Height = TextRenderer.MeasureText(Resources.Download_WebView2, errorMessage.Font).Height;
downloadLink.ForeColor = Settings.TextColor;
Controls.Add(downloadLink);
}
});
}
else
{
Logger.LogInfo("File is too big to display. Showing error message");
AddTextBoxControl(Resources.Max_File_Size_Error.Replace("%1", (_settings.MaxFileSize / 1000).ToString(CultureInfo.CurrentCulture), StringComparison.InvariantCulture));
}
}
catch (UnauthorizedAccessException e)
{
Logger.LogError(e.Message);
AddTextBoxControl(Resources.Access_Denied_Exception_Message);
}
catch (Exception e)
{
Logger.LogError(e.Message);
string errorMessage = Resources.Exception_Occurred;
errorMessage += e.Message;
errorMessage += "\n" + e.Source;
errorMessage += "\n" + e.StackTrace;
AddTextBoxControl(errorMessage);
}
this.Resize += FormResize;
}
private async void CoreWebView2_NewWindowRequested(object sender, CoreWebView2NewWindowRequestedEventArgs e)
{
// Monaco opens URI in a new window. We open the URI in the default web browser.
if (e.Uri != null && e.IsUserInitiated)
{
e.Handled = true;
await Launcher.LaunchUriAsync(new Uri(e.Uri));
}
}
/// <summary>
/// This event sets the height and width of the webview to the size of the form
/// </summary>
public void FormResize(object sender, EventArgs e)
{
_webView.Height = this.Height;
_webView.Width = this.Width;
this.Update();
}
/// <summary>
/// This event initializes the webview and sets various settings
/// </summary>
[STAThread]
private void WebView2Init(object sender, CoreWebView2NavigationCompletedEventArgs e)
{
// Checks if already navigated
if (!_hasNavigated)
{
Logger.LogInfo("Setting WebView2 settings");
CoreWebView2Settings settings = (sender as WebView2).CoreWebView2.Settings;
#if DEBUG
// Enable developer tools and context menu for debugging
settings.AreDefaultContextMenusEnabled = true;
settings.AreDevToolsEnabled = true;
#else
// Disable context menu
settings.AreDefaultContextMenusEnabled = false;
// Disable developer tools
settings.AreDevToolsEnabled = false;
#endif
// Disable script dialogs (like alert())
settings.AreDefaultScriptDialogsEnabled = false;
// Enables JavaScript
settings.IsScriptEnabled = true;
// Disable zoom with ctrl and scroll
settings.IsZoomControlEnabled = false;
// Disable developer menu
settings.IsBuiltInErrorPageEnabled = false;
// Disable status bar
settings.IsStatusBarEnabled = false;
Logger.LogInfo("Remove loading elements");
Controls.Remove(_loading);
Controls.Remove(_loadingBar);
Controls.Remove(_loadingBackground);
#if DEBUG
_webView.CoreWebView2.OpenDevToolsWindow();
Logger.LogInfo("Opened Dev Tools window, because solution was built in debug mode");
#endif
_loadingBar.Value = 80;
this.Update();
}
}
/// <summary>
/// This event cancels every navigation inside the webview
/// </summary>
[STAThread]
private void NavigationStarted(object sender, CoreWebView2NavigationStartingEventArgs e)
{
// Prevents navigation if already one done to index.html
if (_hasNavigated)
{
e.Cancel = false;
Logger.LogInfo("Stopped navigation from user");
}
// If it has navigated to index.html it stops further navigations
if (e.Uri == "about:blank")
{
_hasNavigated = true;
}
}
private void SetBackground()
{
Logger.LogTrace();
this.BackColor = Settings.BackgroundColor;
}
private void InitializeLoadingScreen()
{
Logger.LogTrace();
_loadingBackground = new Label();
_loadingBackground.BackColor = Settings.BackgroundColor;
_loadingBackground.Width = this.Width;
_loadingBackground.Height = this.Height;
Controls.Add(_loadingBackground);
_loadingBackground.BringToFront();
_loadingBar = new ProgressBar();
_loadingBar.Width = this.Width - 10;
_loadingBar.Location = new Point(5, this.Height / 2);
_loadingBar.Maximum = 100;
_loadingBar.Value = 10;
Controls.Add(_loadingBar);
_loading = new Label();
_loading.Text = Resources.Loading_Screen_Message;
_loading.Width = this.Width;
_loading.Height = 45;
_loading.Location = new Point(0, _loadingBar.Location.Y - _loading.Height);
_loading.TextAlign = ContentAlignment.TopCenter;
_loading.Font = new Font("MS Sans Serif", 16, FontStyle.Bold);
_loading.ForeColor = Settings.TextColor;
Controls.Add(_loading);
_loading.BringToFront();
_loadingBar.BringToFront();
this.Update();
Logger.LogInfo("Loading screen initialized");
}
private void InitializeIndexFileAndSelectedFile(string filePath)
{
Logger.LogInfo("Starting getting monaco language id out of filetype");
_vsCodeLangSet = FileHandler.GetLanguage(Path.GetExtension(filePath));
DetectionResult result = CharsetDetector.DetectFromFile(filePath);
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// Check if the detected encoding is not null, otherwise default to UTF-8
Encoding encodingToUse = result.Detected?.Encoding ?? Encoding.UTF8;
using (StreamReader fileReader = new StreamReader(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), encodingToUse))
{
Logger.LogInfo("Starting reading requested file");
var fileContent = fileReader.ReadToEnd();
if (_settings.TryFormat)
{
var formatter = FilePreviewCommon.MonacoHelper.Formatters.SingleOrDefault(f => f.LangSet == _vsCodeLangSet);
if (formatter != null)
{
try
{
fileContent = formatter.Format(fileContent);
}
catch (Exception ex)
{
Logger.LogError($"Failed to apply formatting to {filePath}", ex);
}
}
}
fileReader.Close();
_base64FileCode = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(fileContent));
Logger.LogInfo("Reading requested file ended");
}
// prepping index html to load in
_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_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);
_html = _html.Replace("[[PT_CODE]]", _base64FileCode, StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_STICKY_SCROLL]]", _settings.StickyScroll ? "1" : "0", StringComparison.InvariantCulture);
_html = _html.Replace("[[PT_URL]]", FilePreviewCommon.MonacoHelper.VirtualHostName, StringComparison.InvariantCulture);
}
private async void DownloadLink_Click(object sender, EventArgs e)
{
await Launcher.LaunchUriAsync(new Uri("https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section"));
Logger.LogTrace();
}
/// <summary>
/// Occurs when RichtextBox is resized.
/// </summary>
/// <param name="sender">Reference to resized control.</param>
/// <param name="e">Provides data for the ContentsResized event.</param>
private void RTBContentsResized(object sender, ContentsResizedEventArgs e)
{
var richTextBox = sender as RichTextBox;
richTextBox.Height = e.NewRectangle.Height + 5;
}
/// <summary>
/// Occurs when form is resized.
/// </summary>
/// <param name="sender">Reference to resized control.</param>
/// <param name="e">Provides data for the resize event.</param>
private void FormResized(object sender, EventArgs e)
{
if (_infoBarAdded)
{
_textBox.Width = Width;
}
}
/// <summary>
/// Adds a Text Box in Controls for showing information about blocked elements.
/// </summary>
/// <param name="message">Message to be displayed in textbox.</param>
private void AddTextBoxControl(string message)
{
Controls.Remove(_loading);
Controls.Remove(_loadingBar);
Controls.Remove(_loadingBackground);
_textBox = new RichTextBox();
_textBox.Text = message;
_textBox.BackColor = Color.LightYellow;
_textBox.Multiline = true;
_textBox.Dock = DockStyle.Top;
_textBox.ReadOnly = true;
_textBox.ContentsResized += RTBContentsResized;
_textBox.ScrollBars = RichTextBoxScrollBars.None;
_textBox.BorderStyle = BorderStyle.None;
Controls.Add(_textBox);
}
}
}