diff --git a/OPTIONS.md b/OPTIONS.md
index a5fd6b0cb..d7ef8f3d8 100644
--- a/OPTIONS.md
+++ b/OPTIONS.md
@@ -293,6 +293,13 @@ These keys are also stored in `QuickLook.config`.
- Example:
- `13`
+### ``
+- Default: empty (the default background)
+- Type: `String`
+- Description: Custom background color of the text viewer. The value is parsed by WPF `ColorConverter` (`#AARRGGBB` is accepted, so it can be translucent). It only applies to the dark theme. The "Background" entry in the More menu cycles through six presets and stores the chosen value here.
+- Example:
+ - `#B4141414`
+
## Notes
- All option names are case-sensitive and stored as XML element names under ``.
diff --git a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.MoreMenu.cs b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.MoreMenu.cs
index 5eee34293..ddad93002 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.MoreMenu.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.MoreMenu.cs
@@ -74,5 +74,48 @@ public IEnumerable GetMenuItems()
if (reopen is not null)
yield return reopen;
+
+ if (_tvp is not null)
+ {
+ yield return new MoreMenuItem()
+ {
+ Icon = FontSymbols.Save,
+ Header = TranslationHelper.Get("MW_SaveText", translationFile),
+ Command = new RelayCommand(() => _tvp?.Save()),
+ };
+
+ var background = new MoreMenuItem()
+ {
+ Icon = FontSymbols.Color,
+ Header = GetBackgroundHeader(translationFile),
+ };
+ background.Command = new RelayCommand(() =>
+ {
+ // Cycle through the presets; a custom value from the config file restarts at the lightest one
+ var presets = TextViewerPanel.BackgroundPresets;
+ var index = Array.FindIndex(presets, p => p.Equals(TextViewerPanel.BackgroundColor, StringComparison.OrdinalIgnoreCase));
+ TextViewerPanel.BackgroundColor = presets[(index + 1) % presets.Length];
+ _tvp?.ApplyBackground();
+ background.Header = GetBackgroundHeader(translationFile);
+ });
+ yield return background;
+ }
+ }
+
+ private static string GetBackgroundHeader(string translationFile)
+ {
+ var index = Array.FindIndex(TextViewerPanel.BackgroundPresets,
+ p => p.Equals(TextViewerPanel.BackgroundColor, StringComparison.OrdinalIgnoreCase));
+ var name = index switch
+ {
+ 0 => TranslationHelper.Get("MW_BackgroundLight", translationFile),
+ 1 => TranslationHelper.Get("MW_BackgroundMedium", translationFile),
+ 2 => TranslationHelper.Get("MW_BackgroundDefault", translationFile),
+ 3 => TranslationHelper.Get("MW_BackgroundDark", translationFile),
+ 4 => TranslationHelper.Get("MW_BackgroundDarker", translationFile),
+ 5 => TranslationHelper.Get("MW_BackgroundBlack", translationFile),
+ _ => TextViewerPanel.BackgroundColor,
+ };
+ return $"{TranslationHelper.Get("MW_Background", translationFile)}: {name}";
}
}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.cs b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.cs
index 92bc33737..55ffe4f80 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Plugin.cs
@@ -69,7 +69,7 @@ public bool CanHandle(string path)
public void Prepare(string path, ContextObject context)
{
- context.PreferredSize = new Size { Width = 800, Height = 600 };
+ context.PreferredSize = new Size { Width = 800, Height = Math.Min(900, SystemParameters.WorkArea.Height * 0.85) };
}
public void View(string path, ContextObject context)
@@ -100,6 +100,7 @@ public void View(string path, ContextObject context)
public void Cleanup()
{
+ _tvp?.PromptSaveIfDirty();
_tvp?.Dispose();
_tvp = null;
}
diff --git a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/TextViewerPanel.cs b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/TextViewerPanel.cs
index 0da7847f1..bb8d56578 100644
--- a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/TextViewerPanel.cs
+++ b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/TextViewerPanel.cs
@@ -43,6 +43,19 @@ namespace QuickLook.Plugin.TextViewer;
public partial class TextViewerPanel : TextEditor, IDisposable
{
private bool _disposed;
+ private string _path;
+ private ContextObject _context;
+ private string _title;
+ private bool _hasBom;
+ private bool _isDirty;
+ private int _escapeLeftEditorTick; // Environment.TickCount of the Escape that left the editor, 0 if none
+ private bool _isDarkHighlighting;
+
+ ///
+ /// Background presets for the dark theme, from lightest to darkest, cycled from the More menu. An empty value keeps the
+ /// window backdrop (transparent); any other "#AARRGGBB" value can also be set in the config file.
+ ///
+ public static readonly string[] BackgroundPresets = ["#30FFFFFF", "#18FFFFFF", "", "#B4141414", "#E60C0C0C", "#FF000000"];
/// Maximum number of characters allowed on a single line before it is truncated.
private const int MAX_LINE_LENGTH = 10000;
@@ -95,12 +108,25 @@ public TextViewerPanel()
?.Stroke = Brushes.Transparent;
ContextMenu = new ContextMenu();
+ // Add "Cut" and "Paste" menu items, only shown while editing.
+ ContextMenu.Items.Add(new MenuItem
+ {
+ Header = TranslationHelper.Get("Editor_Cut", domain: Assembly.GetExecutingAssembly().GetName().Name),
+ Command = ApplicationCommands.Cut,
+ Tag = "EditOnly",
+ });
// Add "Copy" menu item.
ContextMenu.Items.Add(new MenuItem
{
Header = TranslationHelper.Get("Editor_Copy", domain: Assembly.GetExecutingAssembly().GetName().Name),
Command = ApplicationCommands.Copy
});
+ ContextMenu.Items.Add(new MenuItem
+ {
+ Header = TranslationHelper.Get("Editor_Paste", domain: Assembly.GetExecutingAssembly().GetName().Name),
+ Command = ApplicationCommands.Paste,
+ Tag = "EditOnly",
+ });
// Add "Select All" menu item.
ContextMenu.Items.Add(new MenuItem
{
@@ -113,6 +139,12 @@ public TextViewerPanel()
ManipulationStarting += Viewer_ManipulationStarting;
ManipulationDelta += Viewer_ManipulationDelta;
KeyDown += Viewer_KeyDown;
+ PreviewKeyDown += Viewer_PreviewKeyDown;
+ ContextMenuOpening += (_, _) =>
+ {
+ foreach (var item in ContextMenu.Items.OfType