From 5a06bcb47306a7deefd69ad8f816c1bff9081803 Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Tue, 24 Oct 2023 15:32:35 +0200 Subject: [PATCH] [Peek]Add wrap and formatting options for Monaco previewer (#29378) * add options for monaco previewer * fix formatting --- .../Extensions/ApplicationExtensions.cs | 19 ++++ src/modules/peek/Peek.Common/IApp.cs | 12 +++ .../Models/IPreviewSettings.cs | 13 +++ .../Models/PreviewSettings.cs | 89 +++++++++++++++++++ .../Previewers/PreviewerFactory.cs | 12 ++- .../Helpers/MonacoHelper.cs | 32 +++++-- .../WebBrowserPreviewer.cs | 10 ++- src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs | 7 +- .../peek/Peek.UI/PeekXAML/MainWindow.xaml.cs | 10 ++- .../peek/Peek.UI/Services/UserSettings.cs | 2 +- .../PeekPreviewSettings.cs | 40 +++++++++ .../SettingsXAML/Views/PeekPage.xaml | 19 ++++ .../Settings.UI/Strings/en-us/Resources.resw | 18 ++++ .../Settings.UI/ViewModels/PeekViewModel.cs | 54 +++++++++-- 14 files changed, 313 insertions(+), 24 deletions(-) create mode 100644 src/modules/peek/Peek.Common/Extensions/ApplicationExtensions.cs create mode 100644 src/modules/peek/Peek.Common/IApp.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Models/IPreviewSettings.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Models/PreviewSettings.cs create mode 100644 src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs diff --git a/src/modules/peek/Peek.Common/Extensions/ApplicationExtensions.cs b/src/modules/peek/Peek.Common/Extensions/ApplicationExtensions.cs new file mode 100644 index 0000000000..ab3ae3f79b --- /dev/null +++ b/src/modules/peek/Peek.Common/Extensions/ApplicationExtensions.cs @@ -0,0 +1,19 @@ +// 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 Microsoft.UI.Xaml; + +namespace Peek.Common.Extensions +{ + public static class ApplicationExtensions + { + /// + /// Get registered services at the application level from anywhere + public static T GetService(this Application application) + where T : class + { + return (application as IApp)!.GetService(); + } + } +} diff --git a/src/modules/peek/Peek.Common/IApp.cs b/src/modules/peek/Peek.Common/IApp.cs new file mode 100644 index 0000000000..8f6c9e3441 --- /dev/null +++ b/src/modules/peek/Peek.Common/IApp.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Peek.Common +{ + public interface IApp + { + public T GetService() + where T : class; + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Models/IPreviewSettings.cs b/src/modules/peek/Peek.FilePreviewer/Models/IPreviewSettings.cs new file mode 100644 index 0000000000..1f26f1f3ef --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Models/IPreviewSettings.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Peek.FilePreviewer.Models +{ + public interface IPreviewSettings + { + public bool SourceCodeWrapText { get; } + + public bool SourceCodeTryFormat { get; } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Models/PreviewSettings.cs b/src/modules/peek/Peek.FilePreviewer/Models/PreviewSettings.cs new file mode 100644 index 0000000000..37358c8ac3 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Models/PreviewSettings.cs @@ -0,0 +1,89 @@ +// 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.IO; +using System.IO.Abstractions; +using System.Threading; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; +using Settings.UI.Library; + +namespace Peek.FilePreviewer.Models +{ + public class PreviewSettings : IPreviewSettings + { + private const int MaxNumberOfRetry = 5; + + private readonly ISettingsUtils _settingsUtils; + private readonly IFileSystemWatcher _watcher; + private readonly object _loadingSettingsLock = new(); + + public bool SourceCodeWrapText { get; private set; } + + public bool SourceCodeTryFormat { get; private set; } + + public PreviewSettings() + { + _settingsUtils = new SettingsUtils(); + SourceCodeWrapText = false; + SourceCodeTryFormat = false; + + LoadSettingsFromJson(); + + _watcher = Helper.GetFileWatcher(PeekSettings.ModuleName, PeekPreviewSettings.FileName, () => LoadSettingsFromJson()); + } + + private void LoadSettingsFromJson() + { + lock (_loadingSettingsLock) + { + var retry = true; + var retryCount = 0; + + while (retry) + { + try + { + retryCount++; + + if (!_settingsUtils.SettingsExists(PeekSettings.ModuleName, PeekPreviewSettings.FileName)) + { + Logger.LogInfo("Peek preview-settings.json was missing, creating a new one"); + var defaultSettings = new PeekPreviewSettings(); + _settingsUtils.SaveSettings(defaultSettings.ToJsonString(), PeekSettings.ModuleName, PeekPreviewSettings.FileName); + } + + var settings = _settingsUtils.GetSettingsOrDefault(PeekSettings.ModuleName, PeekPreviewSettings.FileName); + if (settings != null) + { + SourceCodeWrapText = settings.SourceCodeWrapText.Value; + SourceCodeTryFormat = settings.SourceCodeTryFormat.Value; + } + + retry = false; + } + catch (IOException e) + { + if (retryCount > MaxNumberOfRetry) + { + retry = false; + Logger.LogError($"Failed to deserialize preview settings, Retrying {e.Message}", e); + } + else + { + Thread.Sleep(500); + } + } + catch (Exception ex) + { + retry = false; + Logger.LogError("Failed to read changed preview settings", ex); + } + } + } + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs index 8a505d9f9c..661fbb4360 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using Microsoft.PowerToys.Telemetry; +using Microsoft.UI.Xaml; +using Peek.Common.Extensions; using Peek.Common.Models; +using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers.Archives; using Peek.UI.Telemetry.Events; @@ -11,6 +14,13 @@ namespace Peek.FilePreviewer.Previewers { public class PreviewerFactory { + private readonly IPreviewSettings _previewSettings; + + public PreviewerFactory() + { + _previewSettings = Application.Current.GetService(); + } + public IPreviewer Create(IFileSystemItem file) { if (ImagePreviewer.IsFileTypeSupported(file.Extension)) @@ -23,7 +33,7 @@ namespace Peek.FilePreviewer.Previewers } else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension)) { - return new WebBrowserPreviewer(file); + return new WebBrowserPreviewer(file, _previewSettings); } else if (ArchivePreviewer.IsFileTypeSupported(file.Extension)) { diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/Helpers/MonacoHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/Helpers/MonacoHelper.cs index 57c72517d4..deff795baf 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/Helpers/MonacoHelper.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/Helpers/MonacoHelper.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.Json; using Common.UI; using ManagedCommon; @@ -23,11 +24,11 @@ namespace Peek.FilePreviewer.Previewers JsonDocument languageListDocument = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguages(); JsonElement languageList = languageListDocument.RootElement.GetProperty("list"); foreach (JsonElement e in languageList.EnumerateArray()) - { - if (e.TryGetProperty("extensions", out var extensions)) { - for (int j = 0; j < extensions.GetArrayLength(); j++) + if (e.TryGetProperty("extensions", out var extensions)) { + for (int j = 0; j < extensions.GetArrayLength(); j++) + { set.Add(extensions[j].ToString()); } } @@ -44,15 +45,32 @@ namespace Peek.FilePreviewer.Previewers /// /// Prepares temp html for the previewing /// - public static string PreviewTempFile(string fileText, string extension, string tempFolder) + public static string PreviewTempFile(string fileText, string extension, string tempFolder, bool tryFormat, bool wrapText) { // TODO: check if file is too big, add MaxFileSize to settings - return InitializeIndexFileAndSelectedFile(fileText, extension, tempFolder); + return InitializeIndexFileAndSelectedFile(fileText, extension, tempFolder, tryFormat, wrapText); } - private static string InitializeIndexFileAndSelectedFile(string fileContent, string extension, string tempFolder) + private static string InitializeIndexFileAndSelectedFile(string fileContent, string extension, string tempFolder, bool tryFormat, bool wrapText) { string vsCodeLangSet = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguage(extension); + + if (tryFormat) + { + var formatter = Microsoft.PowerToys.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", ex); + } + } + } + string base64FileCode = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(fileContent)); string theme = ThemeManager.GetWindowsBaseColor().ToLowerInvariant(); @@ -60,7 +78,7 @@ namespace Peek.FilePreviewer.Previewers string html = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.ReadIndexHtml(); html = html.Replace("[[PT_LANG]]", vsCodeLangSet, StringComparison.InvariantCulture); - html = html.Replace("[[PT_WRAP]]", "1", StringComparison.InvariantCulture); // TODO: add to settings + html = html.Replace("[[PT_WRAP]]", wrapText ? "1" : "0", StringComparison.InvariantCulture); html = html.Replace("[[PT_THEME]]", theme, StringComparison.InvariantCulture); html = html.Replace("[[PT_CODE]]", base64FileCode, StringComparison.InvariantCulture); html = html.Replace("[[PT_URL]]", Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, StringComparison.InvariantCulture); diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs index f13bad1d39..adbcd8eb4c 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/WebBrowserPreviewer/WebBrowserPreviewer.cs @@ -13,13 +13,14 @@ using Peek.Common.Extensions; using Peek.Common.Helpers; using Peek.Common.Models; using Peek.FilePreviewer.Models; -using Windows.Foundation; namespace Peek.FilePreviewer.Previewers { public partial class WebBrowserPreviewer : ObservableObject, IBrowserPreviewer, IDisposable { - private static readonly HashSet _supportedFileTypes = new HashSet + private readonly IPreviewSettings _previewSettings; + + private static readonly HashSet _supportedFileTypes = new() { // Web ".html", @@ -43,8 +44,9 @@ namespace Peek.FilePreviewer.Previewers private bool disposed; - public WebBrowserPreviewer(IFileSystemItem file) + public WebBrowserPreviewer(IFileSystemItem file, IPreviewSettings previewSettings) { + _previewSettings = previewSettings; File = file; Dispatcher = DispatcherQueue.GetForCurrentThread(); } @@ -109,7 +111,7 @@ namespace Peek.FilePreviewer.Previewers if (IsDevFilePreview && !isHtml && !isMarkdown) { var raw = await ReadHelper.Read(File.Path.ToString()); - Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path)); + Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path, _previewSettings.SourceCodeTryFormat, _previewSettings.SourceCodeWrapText)); } else if (isMarkdown) { diff --git a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs index 8e99c57977..c88badde43 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs @@ -8,7 +8,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; +using Peek.Common; using Peek.FilePreviewer; +using Peek.FilePreviewer.Models; using Peek.UI.Telemetry.Events; using Peek.UI.Views; @@ -17,7 +19,7 @@ namespace Peek.UI /// /// Provides application-specific behavior to supplement the default Application class. /// - public partial class App : Application + public partial class App : Application, IApp { public static int PowerToysPID { get; set; } @@ -46,6 +48,7 @@ namespace Peek.UI // Core Services services.AddTransient(); services.AddSingleton(); + services.AddSingleton(); // Views and ViewModels services.AddTransient(); @@ -57,7 +60,7 @@ namespace Peek.UI UnhandledException += App_UnhandledException; } - public static T GetService() + public T GetService() where T : class { if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service) diff --git a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs index 5254e54513..a7dd22440a 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs @@ -8,8 +8,10 @@ using ManagedCommon; using Microsoft.PowerToys.Telemetry; using Microsoft.UI; using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Input; using Peek.Common.Constants; +using Peek.Common.Extensions; using Peek.FilePreviewer.Models; using Peek.UI.Extensions; using Peek.UI.Helpers; @@ -45,7 +47,7 @@ namespace Peek.UI Logger.LogError($"HandleThemeChange exception. Please install .NET 4.", e); } - ViewModel = App.GetService(); + ViewModel = Application.Current.GetService(); NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey); @@ -68,11 +70,11 @@ namespace Peek.UI } } - private void PeekWindow_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) + private void PeekWindow_Activated(object sender, WindowActivatedEventArgs args) { - if (args.WindowActivationState == Microsoft.UI.Xaml.WindowActivationState.Deactivated) + if (args.WindowActivationState == WindowActivationState.Deactivated) { - var userSettings = App.GetService(); + var userSettings = Application.Current.GetService(); if (userSettings.CloseAfterLosingFocus) { Uninitialize(); diff --git a/src/modules/peek/Peek.UI/Services/UserSettings.cs b/src/modules/peek/Peek.UI/Services/UserSettings.cs index d15ed3570e..6e5607c6e9 100644 --- a/src/modules/peek/Peek.UI/Services/UserSettings.cs +++ b/src/modules/peek/Peek.UI/Services/UserSettings.cs @@ -48,7 +48,7 @@ namespace Peek.UI if (!_settingsUtils.SettingsExists(PeekModuleName)) { - Logger.LogInfo("Hosts settings.json was missing, creating a new one"); + Logger.LogInfo("Peek settings.json was missing, creating a new one"); var defaultSettings = new PeekSettings(); defaultSettings.Save(_settingsUtils); } diff --git a/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs b/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs new file mode 100644 index 0000000000..33fc4e270f --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs @@ -0,0 +1,40 @@ +// 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.Text.Json; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Settings.UI.Library +{ + public class PeekPreviewSettings : ISettingsConfig + { + public const string FileName = "preview-settings.json"; + + public BoolProperty SourceCodeWrapText { get; set; } + + public BoolProperty SourceCodeTryFormat { get; set; } + + public PeekPreviewSettings() + { + SourceCodeWrapText = new BoolProperty(false); + SourceCodeTryFormat = new BoolProperty(false); + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + + public string GetModuleName() + { + return PeekSettings.ModuleName; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml index 3b1b9eca1a..74b330682e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PeekPage.xaml @@ -40,6 +40,25 @@ + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 5be16768b9..196a55815f 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -3838,4 +3838,22 @@ Activate by holding the key for the character you want to add an accent to, then Enabled modules + + Preview + + + .cpp, .py, .json, .xml, .csproj, ... + + + Source code files (Monaco) + + + Applies to json and xml. Files remain unchanged. + + + Try to format the source for preview + + + Wrap text + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs index 3e84331da3..64ef0d0186 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PeekViewModel.cs @@ -9,6 +9,7 @@ using global::PowerToys.GPOWrapper; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.ViewModels { @@ -20,6 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels private readonly ISettingsUtils _settingsUtils; private readonly PeekSettings _peekSettings; + private readonly PeekPreviewSettings _peekPreviewSettings; private GpoRuleConfigured _enabledGpoRuleConfiguration; private bool _enabledStateIsGPOConfigured; @@ -46,6 +48,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _peekSettings = new PeekSettings(); } + if (_settingsUtils.SettingsExists(PeekSettings.ModuleName, PeekPreviewSettings.FileName)) + { + _peekPreviewSettings = _settingsUtils.GetSettingsOrDefault(PeekSettings.ModuleName, PeekPreviewSettings.FileName); + } + else + { + _peekPreviewSettings = new PeekPreviewSettings(); + } + InitializeEnabledValue(); SendConfigMSG = ipcMSGCallBackFunc; @@ -137,15 +148,48 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public bool SourceCodeWrapText + { + get => _peekPreviewSettings.SourceCodeWrapText.Value; + set + { + if (_peekPreviewSettings.SourceCodeWrapText.Value != value) + { + _peekPreviewSettings.SourceCodeWrapText.Value = value; + OnPropertyChanged(nameof(SourceCodeWrapText)); + SavePreviewSettings(); + } + } + } + + public bool SourceCodeTryFormat + { + get => _peekPreviewSettings.SourceCodeTryFormat.Value; + set + { + if (_peekPreviewSettings.SourceCodeTryFormat.Value != value) + { + _peekPreviewSettings.SourceCodeTryFormat.Value = value; + OnPropertyChanged(nameof(SourceCodeTryFormat)); + SavePreviewSettings(); + } + } + } + private void NotifySettingsChanged() { // Using InvariantCulture as this is an IPC message SendConfigMSG( - string.Format( - CultureInfo.InvariantCulture, - "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", - PeekSettings.ModuleName, - JsonSerializer.Serialize(_peekSettings))); + string.Format( + CultureInfo.InvariantCulture, + "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", + PeekSettings.ModuleName, + JsonSerializer.Serialize(_peekSettings))); + } + + private void SavePreviewSettings() + { + _settingsUtils.SaveSettings(_peekPreviewSettings.ToJsonString(), PeekSettings.ModuleName, PeekPreviewSettings.FileName); } public void RefreshEnabledState()