using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using Explorer.Application; using Explorer.Domain; using Explorer.Domain.Abstractions; using Microsoft.Extensions.Logging; using Microsoft.Win32; namespace Explorer.Windows; public sealed class WindowsShellContextMenu : IShellContextMenu { private const uint IdCmdFirst = 1; private const uint IdCmdLast = 0x7FFF; private const uint CmfNormal = 0; private const uint CmfExplore = 4; private const uint CmfCanRename = 0x10; private const uint CmfExtendedVerbs = 0x100; private const uint CmfItemMenu = 0x80; private const uint TpmLeftAlign = 0x0000; private const uint TpmRightButton = 0x0002; private const uint TpmReturnCmd = 0x0100; private const uint WmNull = 0x0000; private const uint WmInitMenuPopup = 0x0117; private const uint WmDrawItem = 0x002B; private const uint WmMeasureItem = 0x002C; private const uint WmMenuChar = 0x0120; private const uint WmLButtonDown = 0x0201; private const uint WmLButtonUp = 0x0202; private const uint WmRButtonDown = 0x0204; private const uint WmRButtonUp = 0x0205; private const uint WsPopup = 0x80000000; private const uint WsExToolwindow = 0x00000080; private const uint WsExNoActivate = 0x08000000; private const uint PmRemove = 0x0001; private const int GwlpWndProc = -4; private const int SwShowNormal = 1; private const int SwShowNoActivate = 8; private const int VkLButton = 0x01; private const int VkRButton = 0x02; private static readonly Guid ShellItemId = new("43826d1e-e718-42ee-bc55-a1e261c37bfe"); private static readonly Guid ContextMenuId = new("000214e4-0000-0000-c000-000000000046"); private static readonly Guid SfuiObject = new("3981e224-f559-11d3-8e3a-00c04f6837d5"); private readonly ILogger _logger; private Dictionary? _staticCommands; private IReadOnlyList _paths = []; private nint _oldWndProc; private WndProc? _subclassProc; private IContextMenu2? _menu2; private IContextMenu3? _menu3; public WindowsShellContextMenu(ILogger logger) => _logger = logger; public IReadOnlyList Query(IReadOnlyList paths) { _paths = Normalize(paths); if (_paths.Count == 0) { _staticCommands = null; return []; } try { return ShellContextVerbFilter.Prune(QueryRegistryVerbs(_paths)); } catch (Exception ex) { _logger.LogDebug(ex, "Registry shell verbs failed"); _staticCommands = null; return []; } } public bool TryInvoke(string id, out string? error) { error = null; try { if (id.StartsWith("static:", StringComparison.Ordinal) && _staticCommands is not null && _staticCommands.TryGetValue(id, out var command) && ShellVerbCommand.TrySplit(command, out var exe, out var args)) { foreach (var invocation in ShellVerbCommand.ExpandInvocations(args, _paths)) { Process.Start(new ProcessStartInfo { FileName = exe, Arguments = invocation, UseShellExecute = false }); } return true; } } catch (Exception ex) { error = ex.Message; _logger.LogDebug(ex, "Shell verb invoke failed {Id}", id); return false; } error = "Command is no longer available."; return false; } public bool TryShowFullMenu(IReadOnlyList paths, int screenX, int screenY, nint ownerHwnd, out string? error) { error = null; var normalized = Normalize(paths); if (normalized.Count == 0) { error = "Select a file first."; return false; } nint hMenu = 0; nint host = 0; IContextMenu? menu = null; try { if (!TryCreateShellMenu(normalized, out menu) || menu is null) { error = "Windows could not build the full menu."; return false; } hMenu = CreatePopupMenu(); var hr = menu.QueryContextMenu( hMenu, 0, IdCmdFirst, IdCmdLast, CmfNormal | CmfExplore | CmfCanRename | CmfItemMenu | CmfExtendedVerbs); if (hr < 0) { error = "Windows could not fill the full menu."; return false; } DrainPendingMouseClicks(); WaitForMouseButtonsUp(); // Message-only windows cannot host a visible TrackPopupMenu. Own a tiny popup // from the Workbench HWND so IContextMenu2/3 messages stay off the WPF wndproc. host = CreateWindowEx( WsExToolwindow | WsExNoActivate, "Static", "", WsPopup, screenX, screenY, 1, 1, ownerHwnd, 0, GetModuleHandle(null), 0); var hwnd = host != 0 ? host : ownerHwnd; if (hwnd == 0) { error = "Windows could not open the full menu."; return false; } _menu2 = QueryInterface(menu); _menu3 = QueryInterface(menu); if (host != 0) { ShowWindow(host, SwShowNoActivate); Subclass(host); } SetForegroundWindow(ownerHwnd != 0 ? ownerHwnd : hwnd); var cmd = TrackPopupMenuEx( hMenu, TpmLeftAlign | TpmRightButton | TpmReturnCmd, screenX, screenY, hwnd, 0); PostMessage(hwnd, WmNull, 0, 0); if (cmd < IdCmdFirst) { return true; } return InvokeOffset(menu, cmd - IdCmdFirst, ownerHwnd != 0 ? ownerHwnd : hwnd, out error); } catch (Exception ex) { error = ex.Message; _logger.LogDebug(ex, "Full shell menu failed"); return false; } finally { if (host != 0) { Unsubclass(host); DestroyWindow(host); } _menu2 = null; _menu3 = null; if (hMenu != 0) { DestroyMenu(hMenu); } if (menu is not null) { Marshal.ReleaseComObject(menu); } } } private static IReadOnlyList Normalize(IReadOnlyList paths) => paths .Where(path => !string.IsNullOrWhiteSpace(path) && !LocationRoots.IsVirtual(path)) .Select(PathRules.FromExtended) .Distinct(StringComparer.OrdinalIgnoreCase) .Take(32) .ToList(); private bool InvokeOffset(IContextMenu menu, uint offset, nint hwnd, out string? error) { error = null; var info = new Cminvokecommandinfo { cbSize = Marshal.SizeOf(), fMask = 0, hwnd = hwnd, lpVerb = (nint)offset, lpParameters = null, lpDirectory = null, nShow = SwShowNormal }; var hr = menu.InvokeCommand(ref info); if (hr < 0) { error = "The Windows command failed."; _logger.LogDebug("IContextMenu.InvokeCommand offset={Offset} hr={Hr}", offset, hr); return false; } return true; } private static bool TryCreateShellMenu(IReadOnlyList paths, out IContextMenu? menu) => (paths.Count == 1 && TryCreateItemMenu(paths[0], out menu)) || TryCreateItemArrayMenu(paths, out menu); private static bool TryCreateItemMenu(string path, out IContextMenu? menu) { menu = null; var iidItem = ShellItemId; if (SHCreateItemFromParsingName(path, 0, ref iidItem, out var item) != 0 || item is null) { return false; } try { return TryBindContextMenu(item, out menu); } finally { Marshal.ReleaseComObject(item); } } private static bool TryCreateItemArrayMenu(IReadOnlyList paths, out IContextMenu? menu) { menu = null; var pidls = new List(); try { foreach (var path in paths) { if (SHParseDisplayName(path, 0, out var pidl, 0, out _) != 0 || pidl == 0) { return false; } pidls.Add(pidl); } if (SHCreateShellItemArrayFromIDLists((uint)pidls.Count, pidls.ToArray(), out var array) != 0 || array is null) { return false; } try { return TryBindContextMenu(array, out menu); } finally { Marshal.ReleaseComObject(array); } } catch { return false; } finally { foreach (var pidl in pidls) { ILFree(pidl); } } } private static bool TryBindContextMenu(IShellItem item, out IContextMenu? menu) { var bhid = SfuiObject; var iid = ContextMenuId; return TryWrapContextMenu(item.BindToHandler(0, ref bhid, ref iid, out var unk), unk, out menu); } private static bool TryBindContextMenu(IShellItemArray array, out IContextMenu? menu) { var bhid = SfuiObject; var iid = ContextMenuId; return TryWrapContextMenu(array.BindToHandler(0, ref bhid, ref iid, out var unk), unk, out menu); } private static bool TryWrapContextMenu(int hr, nint unk, out IContextMenu? menu) { menu = null; if (hr != 0 || unk == 0) { return false; } try { menu = (IContextMenu)Marshal.GetObjectForIUnknown(unk); return menu is not null; } finally { Marshal.Release(unk); } } private static void DrainPendingMouseClicks() { while (PeekMessage(out _, 0, WmLButtonDown, WmLButtonUp, PmRemove)) { } while (PeekMessage(out _, 0, WmRButtonDown, WmRButtonUp, PmRemove)) { } } private static void WaitForMouseButtonsUp() { var start = Environment.TickCount64; while (Environment.TickCount64 - start < 250 && ((GetAsyncKeyState(VkLButton) & 0x8000) != 0 || (GetAsyncKeyState(VkRButton) & 0x8000) != 0)) { DrainPendingMouseClicks(); Thread.Sleep(10); } } private IReadOnlyList QueryRegistryVerbs(IReadOnlyList paths) { _staticCommands = new Dictionary(StringComparer.Ordinal); var allFiles = paths.All(File.Exists); var allDirs = paths.All(Directory.Exists); var keys = new List(); if (allFiles) { keys.Add(@"*\shell"); var exts = paths.Select(path => Path.GetExtension(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); if (exts.Count == 1 && !string.IsNullOrEmpty(exts[0])) { keys.AddRange(ProgIdShellKeys(exts[0])); } } else if (allDirs) { keys.Add(@"Directory\shell"); keys.Add(@"Folder\shell"); } var items = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var key in keys) { foreach (var hive in new[] { Registry.ClassesRoot, Registry.CurrentUser }) { var path = hive == Registry.CurrentUser ? @"Software\Classes\" + key : key; using var shell = hive.OpenSubKey(path); if (shell is null) { continue; } foreach (var name in shell.GetSubKeyNames()) { using var verbKey = shell.OpenSubKey(name); if (verbKey is null || verbKey.GetValue("LegacyDisable") is not null || verbKey.GetValue("ProgrammaticAccessOnly") is not null) { continue; } using var commandKey = verbKey.OpenSubKey("command"); var command = commandKey?.GetValue(null) as string; if (string.IsNullOrWhiteSpace(command) || commandKey?.GetValue("DelegateExecute") is not null) { continue; } var label = (verbKey.GetValue("MUIVerb") as string) ?? (verbKey.GetValue(null) as string) ?? name; if (label.StartsWith('@')) { label = name; } var canonical = ShellContextVerbFilter.CanonicalLabel(label); if (!seen.Add(canonical) || !ShellContextVerbFilter.ShouldInclude(name, label)) { continue; } var id = "static:" + items.Count; _staticCommands[id] = command; items.Add(new ShellContextVerb(id, label)); } } } return items; } private static IEnumerable ProgIdShellKeys(string extension) { yield return @"SystemFileAssociations\" + extension + @"\shell"; using var extKey = Registry.ClassesRoot.OpenSubKey(extension); var progId = extKey?.GetValue(null) as string; if (!string.IsNullOrWhiteSpace(progId)) { yield return progId + @"\shell"; } } private void Subclass(nint hwnd) { _subclassProc = OnSubclass; _oldWndProc = SetWindowLongPtr(hwnd, GwlpWndProc, Marshal.GetFunctionPointerForDelegate(_subclassProc)); } private void Unsubclass(nint hwnd) { if (hwnd == 0 || _oldWndProc == 0) { return; } SetWindowLongPtr(hwnd, GwlpWndProc, _oldWndProc); _oldWndProc = 0; _subclassProc = null; } private nint OnSubclass(nint hWnd, uint msg, nint wParam, nint lParam) { if (_menu3 is not null && msg is WmInitMenuPopup or WmDrawItem or WmMeasureItem or WmMenuChar && _menu3.HandleMenuMsg2(msg, wParam, lParam, out var result) == 0) { return result; } if (_menu2 is not null && msg is WmInitMenuPopup or WmDrawItem or WmMeasureItem or WmMenuChar) { _menu2.HandleMenuMsg(msg, wParam, lParam); return 0; } return CallWindowProc(_oldWndProc, hWnd, msg, wParam, lParam); } private static T? QueryInterface(object com) where T : class { var unk = Marshal.GetIUnknownForObject(com); try { var iid = typeof(T).GUID; if (Marshal.QueryInterface(unk, in iid, out var ptr) != 0 || ptr == 0) { return null; } try { return (T)Marshal.GetObjectForIUnknown(ptr); } finally { Marshal.Release(ptr); } } finally { Marshal.Release(unk); } } private delegate nint WndProc(nint hWnd, uint msg, nint wParam, nint lParam); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetForegroundWindow(nint hWnd); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool ShowWindow(nint hWnd, int nCmdShow); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PostMessage(nint hWnd, uint msg, nint wParam, nint lParam); [DllImport("user32.dll")] private static extern short GetAsyncKeyState(int vKey); [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "PeekMessageW")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PeekMessage(out NativeMsg msg, nint hWnd, uint msgMin, uint msgMax, uint remove); [DllImport("user32.dll")] private static extern uint TrackPopupMenuEx(nint hmenu, uint flags, int x, int y, nint hwnd, nint paramsEx); [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW")] private static extern nint SetWindowLongPtr64(nint hWnd, int nIndex, nint dwNewLong); [DllImport("user32.dll", EntryPoint = "SetWindowLongW")] private static extern int SetWindowLong32(nint hWnd, int nIndex, int dwNewLong); private static nint SetWindowLongPtr(nint hWnd, int nIndex, nint value) => nint.Size == 8 ? SetWindowLongPtr64(hWnd, nIndex, value) : SetWindowLong32(hWnd, nIndex, (int)value); [DllImport("user32.dll")] private static extern nint CallWindowProc(nint prev, nint hWnd, uint msg, nint wParam, nint lParam); [DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateWindowExW")] private static extern nint CreateWindowEx( uint exStyle, string className, string windowName, uint style, int x, int y, int width, int height, nint parent, nint menu, nint instance, nint param); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool DestroyWindow(nint hWnd); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern nint GetModuleHandle(string? module); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern int SHParseDisplayName(string name, nint bindCtx, out nint pidl, uint sfgaoIn, out uint sfgaoOut); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern int SHCreateItemFromParsingName( string path, nint bindCtx, [In] ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem item); [DllImport("shell32.dll")] private static extern int SHCreateShellItemArrayFromIDLists(uint cidl, [In] nint[] pidls, out IShellItemArray array); [DllImport("shell32.dll")] private static extern void ILFree(nint pidl); [DllImport("user32.dll")] private static extern nint CreatePopupMenu(); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool DestroyMenu(nint hMenu); [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")] private interface IShellItem { [PreserveSig] int BindToHandler(nint pbc, ref Guid bhid, ref Guid riid, out nint ppv); } [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("b63ea76d-1f85-456f-a19c-48159efa858b")] private interface IShellItemArray { [PreserveSig] int BindToHandler(nint pbc, ref Guid bhid, ref Guid riid, out nint ppv); } [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("000214e4-0000-0000-c000-000000000046")] private interface IContextMenu { [PreserveSig] int QueryContextMenu(nint hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); [PreserveSig] int InvokeCommand(ref Cminvokecommandinfo info); [PreserveSig] int GetCommandString(UIntPtr idCmd, uint uType, nint reserved, nint name, uint cchMax); } [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("000214f4-0000-0000-c000-000000000046")] private interface IContextMenu2 { [PreserveSig] int QueryContextMenu(nint hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); [PreserveSig] int InvokeCommand(ref Cminvokecommandinfo info); [PreserveSig] int GetCommandString(UIntPtr idCmd, uint uType, nint reserved, nint name, uint cchMax); [PreserveSig] int HandleMenuMsg(uint uMsg, nint wParam, nint lParam); } [ComImport] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] [Guid("000214fc-0000-0000-c000-000000000046")] private interface IContextMenu3 { [PreserveSig] int QueryContextMenu(nint hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); [PreserveSig] int InvokeCommand(ref Cminvokecommandinfo info); [PreserveSig] int GetCommandString(UIntPtr idCmd, uint uType, nint reserved, nint name, uint cchMax); [PreserveSig] int HandleMenuMsg(uint uMsg, nint wParam, nint lParam); [PreserveSig] int HandleMenuMsg2(uint uMsg, nint wParam, nint lParam, out nint result); } [StructLayout(LayoutKind.Sequential)] private struct NativeMsg { public nint hwnd; public uint message; public nint wParam; public nint lParam; public uint time; public int ptX; public int ptY; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] private struct Cminvokecommandinfo { public int cbSize; public int fMask; public nint hwnd; public nint lpVerb; public string? lpParameters; public string? lpDirectory; public int nShow; public int dwHotKey; public nint hIcon; } }