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;
///
/// Call from WndProc. Returns true when the message was ours and the native menu should
/// be suppressed.
///
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;
}
/// lParam of a non-client mouse message packs screen coords as two shorts.
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);
}
}