- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc. Fixes edited values staying recoverable by text extraction. - Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode. - Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon. - Remove the upstream author credit and the in-app install button.
180 lines
8.1 KiB
C#
180 lines
8.1 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Interop;
|
|
using System.Windows.Media.Animation;
|
|
|
|
// ============================================================
|
|
// THEMED WINDOWS SYSTEM MENU (from MmdPdfUI/Shell/SystemMenu.cs)
|
|
//
|
|
// WindowChrome turns the title bar into a real native caption, which is what buys free snap,
|
|
// drag and double-click-maximize - but it also means Windows answers a caption right-click
|
|
// (and Alt+Space) with its own stock white HMENU. That menu is drawn by Win32, not WPF, so no
|
|
// brush, style or theme in the app can reach it. The ONLY way to theme it is to suppress the
|
|
// two messages that raise it and show our own.
|
|
//
|
|
// So: swallow them and pop a normal WPF ContextMenu, which picks up the app's implicit
|
|
// ContextMenu / MenuItem / Separator styles for free. Each item posts back the exact
|
|
// WM_SYSCOMMAND the native menu would have sent, so behavior is identical - including Move and
|
|
// Size, which hand off to Windows' own modal drag loops.
|
|
//
|
|
// MmdPdf adaptations: HTCAPTION already lives in Shell/WindowChrome.cs (not redeclared
|
|
// here), the menu comes from MakeThemedMenu() so its TextOptions match the app's other
|
|
// code-built menus, and the kit's Anim.FadeIn is inlined (MmdPdf carries no Anim class).
|
|
// ============================================================
|
|
namespace MmdPdf
|
|
{
|
|
public partial class MainWindow
|
|
{
|
|
private const int WM_NCRBUTTONUP = 0x00A5;
|
|
private const int WM_SYSCOMMAND = 0x0112;
|
|
|
|
private const int SC_SIZE = 0xF000;
|
|
private const int SC_MOVE = 0xF010;
|
|
private const int SC_MINIMIZE = 0xF020;
|
|
private const int SC_MAXIMIZE = 0xF030;
|
|
private const int SC_CLOSE = 0xF060;
|
|
private const int SC_KEYMENU = 0xF100;
|
|
private const int SC_RESTORE = 0xF120;
|
|
|
|
private ContextMenu? _sysMenu;
|
|
|
|
/// <summary>
|
|
/// Call from WndProc. Returns true when the message was ours and the native menu should
|
|
/// be suppressed.
|
|
/// </summary>
|
|
private bool TryHandleSystemMenu(int msg, IntPtr wParam, IntPtr lParam)
|
|
{
|
|
// Right-click on the caption.
|
|
if (msg == WM_NCRBUTTONUP && wParam.ToInt32() == HTCAPTION)
|
|
{
|
|
ShowSystemMenu(ScreenPointFromLParam(lParam));
|
|
return true;
|
|
}
|
|
|
|
// Alt+Space. SC_KEYMENU with a space is the keyboard route to the same menu; masking
|
|
// the low nibble is required because Windows packs state into it.
|
|
if (msg == WM_SYSCOMMAND && (wParam.ToInt64() & 0xFFF0) == SC_KEYMENU && lParam.ToInt64() == ' ')
|
|
{
|
|
ShowSystemMenu(null);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>lParam of a non-client mouse message packs screen coords as two shorts.</summary>
|
|
private static Point ScreenPointFromLParam(IntPtr lParam)
|
|
{
|
|
int v = lParam.ToInt32();
|
|
return new Point((short)(v & 0xFFFF), (short)((v >> 16) & 0xFFFF));
|
|
}
|
|
|
|
private void ShowSystemMenu(Point? screenPoint)
|
|
{
|
|
_sysMenu ??= BuildSystemMenu();
|
|
|
|
bool maximized = WindowState == WindowState.Maximized;
|
|
// Windows grays out what does not apply: you cannot Restore a normal window, cannot
|
|
// Maximize an already-maximized one, and cannot Move or Size while maximized.
|
|
foreach (object o in _sysMenu.Items)
|
|
{
|
|
if (o is not MenuItem mi || mi.Tag is not int cmd) continue;
|
|
mi.IsEnabled = cmd switch
|
|
{
|
|
SC_RESTORE => maximized,
|
|
SC_MAXIMIZE => !maximized,
|
|
SC_MOVE or SC_SIZE => !maximized,
|
|
_ => true,
|
|
};
|
|
}
|
|
|
|
// MmdPdf keeps its implicit ContextMenu/MenuItem styles in MainWindow.xaml's
|
|
// WINDOW resources (not App.xaml), and an Absolute-placement menu has no inheritance
|
|
// context to find them through - it rendered stock white. PlacementTarget supplies
|
|
// that context; with PlacementMode.Absolute it does not affect position.
|
|
_sysMenu.PlacementTarget = this;
|
|
_sysMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Absolute;
|
|
if (screenPoint is { } p)
|
|
{
|
|
// Screen pixels -> DIP, so the menu lands under the cursor at any DPI.
|
|
var src = PresentationSource.FromVisual(this);
|
|
if (src?.CompositionTarget is { } ct) p = ct.TransformFromDevice.Transform(p);
|
|
_sysMenu.HorizontalOffset = p.X;
|
|
_sysMenu.VerticalOffset = p.Y;
|
|
}
|
|
else
|
|
{
|
|
// Keyboard route: hang it under the top-left of the window, like Windows does.
|
|
_sysMenu.HorizontalOffset = Left + 8;
|
|
_sysMenu.VerticalOffset = Top + 36;
|
|
}
|
|
|
|
_sysMenu.IsOpen = true;
|
|
// Inline of the kit's Anim.FadeIn: 150ms opacity ease-out.
|
|
_sysMenu.BeginAnimation(UIElement.OpacityProperty,
|
|
new DoubleAnimation(0, 1, new Duration(TimeSpan.FromMilliseconds(150)))
|
|
{
|
|
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut }
|
|
});
|
|
}
|
|
|
|
private ContextMenu BuildSystemMenu()
|
|
{
|
|
var menu = MakeThemedMenu();
|
|
|
|
// Every codepoint below was verified present in segmdl2.ttf by RENDERING it, not
|
|
// trusted from documentation - this repo has shipped a missing-glyph box before.
|
|
// E923/E921/E922/E8BB are the same four the title-bar caption buttons draw, so the
|
|
// menu and the caption agree. E7C2 is the four-way move arrow, E740 the diagonal
|
|
// resize arrow.
|
|
MenuItem Add(string key, string fallback, int cmd, int glyph, bool danger = false)
|
|
{
|
|
var mi = new MenuItem { Tag = cmd, Padding = new Thickness(12, 7, 24, 7) };
|
|
mi.SetResourceReference(HeaderedItemsControl.HeaderProperty, key);
|
|
// Loc() returns the key itself when a string is missing; fall back to English so a
|
|
// half-translated locale never shows "Str_Sys_Move" in the menu.
|
|
if (Loc(key) == key) mi.Header = fallback;
|
|
|
|
var ico = new TextBlock
|
|
{
|
|
Text = char.ConvertFromUtf32(glyph),
|
|
FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"),
|
|
FontSize = 12,
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
};
|
|
ico.SetResourceReference(TextBlock.ForegroundProperty,
|
|
danger ? "DangerRed" : "MutedTextBrush");
|
|
mi.Icon = ico;
|
|
|
|
mi.Click += (_, _) => SendSysCommand(cmd);
|
|
menu.Items.Add(mi);
|
|
return mi;
|
|
}
|
|
|
|
Add("Str_Sys_Restore", "Restore", SC_RESTORE, 0xE923);
|
|
Add("Str_Sys_Move", "Move", SC_MOVE, 0xE7C2);
|
|
Add("Str_Sys_Size", "Size", SC_SIZE, 0xE740);
|
|
Add("Str_Sys_Minimize", "Minimize", SC_MINIMIZE, 0xE921);
|
|
Add("Str_Sys_Maximize", "Maximize", SC_MAXIMIZE, 0xE922);
|
|
menu.Items.Add(new Separator());
|
|
Add("Str_Sys_Close", "Close", SC_CLOSE, 0xE8BB, danger: true);
|
|
return menu;
|
|
}
|
|
|
|
private void SendSysCommand(int cmd)
|
|
{
|
|
var hwnd = new WindowInteropHelper(this).Handle;
|
|
if (hwnd == IntPtr.Zero) return;
|
|
// Post rather than Send: the menu is still closing, and SC_MOVE / SC_SIZE start a
|
|
// modal loop that must not run inside the click handler.
|
|
PostMessage(hwnd, WM_SYSCOMMAND, new IntPtr(cmd), IntPtr.Zero);
|
|
}
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern bool PostMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
|
|
}
|
|
}
|