-
-
Notifications
You must be signed in to change notification settings - Fork 324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate native Windows context menu directly into the Explorer plugin #2742
Conversation
Do you use the win11 context menu or the old one? |
I believe it's the old menu. |
…enu-item to differentiate them in markup
…ontext-menu-item to differentiate them in markup" This reverts commit f49e952.
…nds to better describe it
…the native context menu
I'll need to take a closer look at the changes to Result tomorrow. |
…the native context menu checkbox
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@check-spelling-bot Report🔴 Please reviewSee the 📂 files view, the 📜action log, or 📝 job summary for details.
See ❌ Event descriptions for more information. If the flagged items are 🤯 false positivesIf items relate to a ...
|
WalkthroughWalkthroughIn the Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant FlowLauncher
participant ContextMenuHelper
User->>+FlowLauncher: Select Search Result
alt Show Context Menu
FlowLauncher->>+ContextMenuHelper: Get Context Menu Items
ContextMenuHelper-->>-FlowLauncher: Context Menu Items
FlowLauncher-->>User: Display Context Menu
end
opt Add Email Attachment Action
FlowLauncher->>User: Open Email Application with Attachment
end
Assessment against linked issues
Poem
Recent review detailsConfiguration used: CodeRabbit UI Files selected for processing (7)
Additional comments not posted (4)
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (invoked as PR comments)
Additionally, you can add CodeRabbit Configration File (
|
🥷 Code experts: onesounds Yusyuriv, onesounds have most 👩💻 activity in the files. See details
Activity based on git-commit:
Knowledge based on git-blame:
Activity based on git-commit:
Knowledge based on git-blame:
Activity based on git-commit:
Knowledge based on git-blame:
Activity based on git-commit:
Knowledge based on git-blame:
Activity based on git-commit:
Knowledge based on git-blame:
Activity based on git-commit:
Knowledge based on git-blame: To learn more about /:\ gitStream - Visit our Docs |
This PR is 92.90322580645162% new code. |
Be a legend 🏆 by adding a before and after screenshot of the changes you made, especially if they are around UI/UX. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Outside diff range and nitpick comments (1)
Plugins/Flow.Launcher.Plugin.Explorer/Views/ExplorerSettings.xaml (1)
355-405
: Ensure consistent UI design and usability for native context menu settings.The UI elements for managing the native Windows context menu (include and exclude patterns) are well-integrated. However, consider adding tooltips or help icons next to these settings to provide users with more information on how to use these settings effectively.
public bool ShowWindowsContextMenu | ||
{ | ||
get => Settings.ShowInlinedWindowsContextMenu; | ||
set | ||
{ | ||
Settings.ShowInlinedWindowsContextMenu = value; | ||
OnPropertyChanged(); | ||
} | ||
} | ||
|
||
public string WindowsContextMenuIncludedItems | ||
{ | ||
get => Settings.WindowsContextMenuIncludedItems; | ||
set | ||
{ | ||
Settings.WindowsContextMenuIncludedItems = value; | ||
OnPropertyChanged(); | ||
} | ||
} | ||
|
||
public string WindowsContextMenuExcludedItems | ||
{ | ||
get => Settings.WindowsContextMenuExcludedItems; | ||
set | ||
{ | ||
Settings.WindowsContextMenuExcludedItems = value; | ||
OnPropertyChanged(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure proper naming for properties reflecting their actual functionality.
The properties ShowWindowsContextMenu
, WindowsContextMenuIncludedItems
, and WindowsContextMenuExcludedItems
are misleading. The getter for ShowWindowsContextMenu
returns Settings.ShowInlinedWindowsContextMenu
, which suggests the property should be named ShowInlinedWindowsContextMenu
to avoid confusion and improve code readability.
- public bool ShowWindowsContextMenu
+ public bool ShowInlinedWindowsContextMenu
- public string WindowsContextMenuIncludedItems
+ public string IncludedWindowsContextMenuItems
- public string WindowsContextMenuExcludedItems
+ public string ExcludedWindowsContextMenuItems
Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public bool ShowWindowsContextMenu | |
{ | |
get => Settings.ShowInlinedWindowsContextMenu; | |
set | |
{ | |
Settings.ShowInlinedWindowsContextMenu = value; | |
OnPropertyChanged(); | |
} | |
} | |
public string WindowsContextMenuIncludedItems | |
{ | |
get => Settings.WindowsContextMenuIncludedItems; | |
set | |
{ | |
Settings.WindowsContextMenuIncludedItems = value; | |
OnPropertyChanged(); | |
} | |
} | |
public string WindowsContextMenuExcludedItems | |
{ | |
get => Settings.WindowsContextMenuExcludedItems; | |
set | |
{ | |
Settings.WindowsContextMenuExcludedItems = value; | |
OnPropertyChanged(); | |
} | |
} | |
public bool ShowInlinedWindowsContextMenu | |
{ | |
get => Settings.ShowInlinedWindowsContextMenu; | |
set | |
{ | |
Settings.ShowInlinedWindowsContextMenu = value; | |
OnPropertyChanged(); | |
} | |
} | |
public string IncludedWindowsContextMenuItems | |
{ | |
get => Settings.WindowsContextMenuIncludedItems; | |
set | |
{ | |
Settings.WindowsContextMenuIncludedItems = value; | |
OnPropertyChanged(); | |
} | |
} | |
public string ExcludedWindowsContextMenuItems | |
{ | |
get => Settings.WindowsContextMenuExcludedItems; | |
set | |
{ | |
Settings.WindowsContextMenuExcludedItems = value; | |
OnPropertyChanged(); | |
} | |
} |
Marshal.ReleaseComObject(shellFolder); | ||
|
||
if (pShellFolder != IntPtr.Zero) | ||
Marshal.Release(pShellFolder); | ||
|
||
if (originalPidl != IntPtr.Zero) | ||
malloc?.Free(originalPidl); | ||
|
||
if (malloc != null) | ||
Marshal.ReleaseComObject(malloc); | ||
} | ||
} | ||
|
||
|
||
private static void ProcessMenuWithIcons(IntPtr hMenu, IContextMenu contextMenu, List<ContextMenuItem> menuItems, string prefix = "") | ||
{ | ||
uint menuCount = GetMenuItemCount(hMenu); | ||
|
||
for (uint i = 0; i < menuCount; i++) | ||
{ | ||
var mii = new MENUITEMINFO | ||
{ | ||
cbSize = (uint)Marshal.SizeOf(typeof(MENUITEMINFO)), | ||
fMask = (uint)(MenuItemInformationMask.Bitmap | MenuItemInformationMask.Ftype | | ||
MenuItemInformationMask.Submenu | MenuItemInformationMask.Id) | ||
}; | ||
|
||
GetMenuItemInfo(hMenu, i, true, ref mii); | ||
var menuText = new StringBuilder(256); | ||
uint result = GetMenuString(hMenu, mii.wID, menuText, menuText.Capacity, 0); | ||
|
||
if (result == 0 || string.IsNullOrWhiteSpace(menuText.ToString())) | ||
{ | ||
continue; | ||
} | ||
|
||
menuText.Replace("&", ""); | ||
|
||
IntPtr hSubMenu = GetSubMenu(hMenu, (int)i); | ||
if (hSubMenu != IntPtr.Zero) | ||
{ | ||
ProcessMenuWithIcons(hSubMenu, contextMenu, menuItems, prefix + menuText + " > "); | ||
} | ||
else if (!string.IsNullOrWhiteSpace(menuText.ToString())) | ||
{ | ||
var commandBuilder = new StringBuilder(256); | ||
contextMenu.GetCommandString( | ||
mii.wID - ContextMenuStartId, | ||
(uint)GetCommandStringFlags.Verb, | ||
IntPtr.Zero, | ||
commandBuilder, | ||
commandBuilder.Capacity | ||
); | ||
if (IgnoredContextMenuCommands.Contains(commandBuilder.ToString(), StringComparer.OrdinalIgnoreCase)) | ||
{ | ||
continue; | ||
} | ||
|
||
ImageSource icon = null; | ||
if (mii.hbmpItem != IntPtr.Zero) | ||
{ | ||
icon = GetBitmapSourceFromHBitmap(mii.hbmpItem); | ||
} | ||
else if (mii.hbmpChecked != IntPtr.Zero) | ||
{ | ||
icon = GetBitmapSourceFromHBitmap(mii.hbmpChecked); | ||
} | ||
|
||
menuItems.Add(new ContextMenuItem(prefix + menuText, icon, mii.wID)); | ||
} | ||
} | ||
} | ||
|
||
private static BitmapSource GetBitmapSourceFromHBitmap(IntPtr hBitmap) | ||
{ | ||
try | ||
{ | ||
var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap( | ||
hBitmap, | ||
IntPtr.Zero, | ||
Int32Rect.Empty, | ||
BitmapSizeOptions.FromWidthAndHeight(16, 16) | ||
); | ||
|
||
if (!DeleteObject(hBitmap)) | ||
{ | ||
throw new Exception("Failed to delete HBitmap."); | ||
} | ||
|
||
return bitmapSource; | ||
} | ||
catch (COMException) | ||
{ | ||
// ignore | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure proper error handling and resource management in native Windows API interactions.
The implementation of native Windows API interactions in ExecuteContextMenuItem
and GetContextMenuWithIcons
methods is thorough, but consider enhancing error handling by logging errors or providing more detailed user feedback on failures. Additionally, ensure all resources (like handles and pointers) are correctly managed to avoid memory leaks.
- if (hr != 0) throw new Exception("SHParseDisplayName failed");
+ if (hr != 0)
+ {
+ Logger.LogError($"SHParseDisplayName failed with HRESULT: {hr}");
+ throw new Exception($"SHParseDisplayName failed with HRESULT: {hr}");
+ }
Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
public static class ShellContextMenuDisplayHelper | |
{ | |
#region DllImport | |
[DllImport("shell32.dll")] | |
private static extern Int32 SHGetMalloc(out IntPtr hObject); | |
[DllImport("shell32.dll")] | |
private static extern Int32 SHParseDisplayName( | |
[MarshalAs(UnmanagedType.LPWStr)] string pszName, | |
IntPtr pbc, | |
out IntPtr ppidl, | |
UInt32 sfgaoIn, | |
out UInt32 psfgaoOut | |
); | |
[DllImport("shell32.dll")] | |
private static extern Int32 SHBindToParent( | |
IntPtr pidl, | |
[MarshalAs(UnmanagedType.LPStruct)] Guid riid, | |
out IntPtr ppv, | |
ref IntPtr ppidlLast | |
); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern IntPtr CreatePopupMenu(); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern bool DestroyMenu(IntPtr hMenu); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern uint GetMenuItemCount(IntPtr hMenu); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern uint GetMenuString( | |
IntPtr hMenu, uint uIDItem, StringBuilder lpString, int nMaxCount, uint uFlag | |
); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern bool GetMenuItemInfo(IntPtr hMenu, uint uItem, bool fByPosition, ref MENUITEMINFO lpmii); | |
[DllImport("gdi32.dll")] | |
private static extern bool DeleteObject(IntPtr hObject); | |
#endregion | |
#region Constants | |
private const uint ContextMenuStartId = 0x0001; | |
private const uint ContextMenuEndId = 0x7FFF; | |
private static readonly string[] IgnoredContextMenuCommands = | |
{ | |
// We haven't managed to make these work, so we don't display them in the context menu. | |
"Share", | |
"Windows.ModernShare", | |
"PinToStartScreen", | |
"CopyAsPath", | |
// Hide functionality provided by the Explorer plugin itself | |
"Copy", | |
"Delete" | |
}; | |
#endregion | |
#region Enums | |
[Flags] | |
enum ContextMenuFlags : uint | |
{ | |
Normal = 0x00000000, | |
DefaultOnly = 0x00000001, | |
VerbsOnly = 0x00000002, | |
Explore = 0x00000004, | |
NoVerbs = 0x00000008, | |
CanRename = 0x00000010, | |
NoDefault = 0x00000020, | |
IncludeStatic = 0x00000040, | |
ItemMenu = 0x00000080, | |
ExtendedVerbs = 0x00000100, | |
DisabledVerbs = 0x00000200, | |
AsyncVerbState = 0x00000400, | |
OptimizeForInvoke = 0x00000800, | |
SyncCascadeMenu = 0x00001000, | |
DoNotPickDefault = 0x00002000, | |
Reserved = 0xffff0000 | |
} | |
[Flags] | |
enum ContextMenuInvokeCommandFlags : uint | |
{ | |
Icon = 0x00000010, | |
Hotkey = 0x00000020, | |
FlagNoUi = 0x00000400, | |
Unicode = 0x00004000, | |
NoConsole = 0x00008000, | |
AsyncOk = 0x00100000, | |
NoZoneChecks = 0x00800000, | |
ShiftDown = 0x10000000, | |
ControlDown = 0x40000000, | |
FlagLogUsage = 0x04000000, | |
PointInvoke = 0x20000000 | |
} | |
[Flags] | |
enum MenuItemInformationMask : uint | |
{ | |
Bitmap = 0x00000080, | |
Checkmarks = 0x00000008, | |
Data = 0x00000020, | |
Ftype = 0x00000100, | |
Id = 0x00000002, | |
State = 0x00000001, | |
String = 0x00000040, | |
Submenu = 0x00000004, | |
Type = 0x00000010 | |
} | |
enum MenuItemFtype : uint | |
{ | |
Bitmap = 0x00000004, | |
MenuBarBreak = 0x00000020, | |
MenuBreak = 0x00000040, | |
OwnerDraw = 0x00000100, | |
RadioCheck = 0x00000200, | |
RightJustify = 0x00004000, | |
RightOrder = 0x00002000, | |
Separator = 0x00000800, | |
String = 0x00000000, | |
} | |
enum GetCommandStringFlags : uint | |
{ | |
VerbA = 0x00000000, | |
HelpTextA = 0x00000001, | |
ValidateA = 0x00000002, | |
Unicode = VerbW, | |
Verb = VerbW, | |
VerbW = 0x00000004, | |
HelpText = HelpTextW, | |
HelpTextW = 0x00000005, | |
Validate = ValidateW, | |
ValidateW = 0x00000006, | |
VerbIconW = 0x00000014 | |
} | |
#endregion | |
private static IMalloc GetMalloc() | |
{ | |
SHGetMalloc(out var pMalloc); | |
return (IMalloc)Marshal.GetTypedObjectForIUnknown(pMalloc, typeof(IMalloc)); | |
} | |
public static void ExecuteContextMenuItem(string fileName, uint menuItemId) | |
{ | |
IMalloc malloc = null; | |
IntPtr originalPidl = IntPtr.Zero; | |
IntPtr pShellFolder = IntPtr.Zero; | |
IntPtr pContextMenu = IntPtr.Zero; | |
IntPtr hMenu = IntPtr.Zero; | |
IContextMenu contextMenu = null; | |
IShellFolder shellFolder = null; | |
try | |
{ | |
malloc = GetMalloc(); | |
var hr = SHParseDisplayName(fileName, IntPtr.Zero, out var pidl, 0, out _); | |
if (hr != 0) throw new Exception("SHParseDisplayName failed"); | |
originalPidl = pidl; | |
var guid = typeof(IShellFolder).GUID; | |
hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); | |
if (hr != 0) throw new Exception("SHBindToParent failed"); | |
shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); | |
hr = shellFolder.GetUIObjectOf( | |
IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu | |
); | |
if (hr != 0) throw new Exception("GetUIObjectOf failed"); | |
contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); | |
hMenu = CreatePopupMenu(); | |
contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); | |
var directory = Path.GetDirectoryName(fileName); | |
var invokeCommandInfo = new CMINVOKECOMMANDINFO | |
{ | |
cbSize = (uint)Marshal.SizeOf(typeof(CMINVOKECOMMANDINFO)), | |
fMask = (uint)ContextMenuInvokeCommandFlags.Unicode, | |
hwnd = IntPtr.Zero, | |
lpVerb = (IntPtr)(menuItemId - ContextMenuStartId), | |
lpParameters = null, | |
lpDirectory = null, | |
nShow = 1, | |
hIcon = IntPtr.Zero, | |
}; | |
hr = contextMenu.InvokeCommand(ref invokeCommandInfo); | |
if (hr != 0) | |
{ | |
throw new Exception($"InvokeCommand failed with code {hr:X}"); | |
} | |
} | |
finally | |
{ | |
if (hMenu != IntPtr.Zero) | |
DestroyMenu(hMenu); | |
if (contextMenu != null) | |
Marshal.ReleaseComObject(contextMenu); | |
if (pContextMenu != IntPtr.Zero) | |
Marshal.Release(pContextMenu); | |
if (shellFolder != null) | |
Marshal.ReleaseComObject(shellFolder); | |
if (pShellFolder != IntPtr.Zero) | |
Marshal.Release(pShellFolder); | |
if (originalPidl != IntPtr.Zero) | |
malloc?.Free(originalPidl); | |
if (malloc != null) | |
Marshal.ReleaseComObject(malloc); | |
} | |
} | |
public static List<ContextMenuItem> GetContextMenuWithIcons(string filePath) | |
{ | |
IMalloc malloc = null; | |
IntPtr originalPidl = IntPtr.Zero; | |
IntPtr pShellFolder = IntPtr.Zero; | |
IntPtr pContextMenu = IntPtr.Zero; | |
IntPtr hMenu = IntPtr.Zero; | |
IShellFolder shellFolder = null; | |
IContextMenu contextMenu = null; | |
try | |
{ | |
malloc = GetMalloc(); | |
var hr = SHParseDisplayName(filePath, IntPtr.Zero, out var pidl, 0, out _); | |
if (hr != 0) throw new Exception("SHParseDisplayName failed"); | |
originalPidl = pidl; | |
var guid = typeof(IShellFolder).GUID; | |
hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); | |
if (hr != 0) throw new Exception("SHBindToParent failed"); | |
shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); | |
hr = shellFolder.GetUIObjectOf( | |
IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu | |
); | |
if (hr != 0) throw new Exception("GetUIObjectOf failed"); | |
contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); | |
// Without waiting, some items, such as "Send to > Documents", don't always appear, which shifts item ids | |
// even though it shouldn't. Please replace this if you find a better way to fix this bug. | |
Thread.Sleep(200); | |
hMenu = CreatePopupMenu(); | |
contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); | |
var menuItems = new List<ContextMenuItem>(); | |
ProcessMenuWithIcons(hMenu, contextMenu, menuItems); | |
return menuItems; | |
} | |
finally | |
{ | |
if (hMenu != IntPtr.Zero) | |
DestroyMenu(hMenu); | |
if (contextMenu != null) | |
Marshal.ReleaseComObject(contextMenu); | |
if (pContextMenu != IntPtr.Zero) | |
Marshal.Release(pContextMenu); | |
if (shellFolder != null) | |
Marshal.ReleaseComObject(shellFolder); | |
if (pShellFolder != IntPtr.Zero) | |
Marshal.Release(pShellFolder); | |
if (originalPidl != IntPtr.Zero) | |
malloc?.Free(originalPidl); | |
if (malloc != null) | |
Marshal.ReleaseComObject(malloc); | |
} | |
} | |
private static void ProcessMenuWithIcons(IntPtr hMenu, IContextMenu contextMenu, List<ContextMenuItem> menuItems, string prefix = "") | |
{ | |
uint menuCount = GetMenuItemCount(hMenu); | |
for (uint i = 0; i < menuCount; i++) | |
{ | |
var mii = new MENUITEMINFO | |
{ | |
cbSize = (uint)Marshal.SizeOf(typeof(MENUITEMINFO)), | |
fMask = (uint)(MenuItemInformationMask.Bitmap | MenuItemInformationMask.Ftype | | |
MenuItemInformationMask.Submenu | MenuItemInformationMask.Id) | |
}; | |
GetMenuItemInfo(hMenu, i, true, ref mii); | |
var menuText = new StringBuilder(256); | |
uint result = GetMenuString(hMenu, mii.wID, menuText, menuText.Capacity, 0); | |
if (result == 0 || string.IsNullOrWhiteSpace(menuText.ToString())) | |
{ | |
continue; | |
} | |
menuText.Replace("&", ""); | |
IntPtr hSubMenu = GetSubMenu(hMenu, (int)i); | |
if (hSubMenu != IntPtr.Zero) | |
{ | |
ProcessMenuWithIcons(hSubMenu, contextMenu, menuItems, prefix + menuText + " > "); | |
} | |
else if (!string.IsNullOrWhiteSpace(menuText.ToString())) | |
{ | |
var commandBuilder = new StringBuilder(256); | |
contextMenu.GetCommandString( | |
mii.wID - ContextMenuStartId, | |
(uint)GetCommandStringFlags.Verb, | |
IntPtr.Zero, | |
commandBuilder, | |
commandBuilder.Capacity | |
); | |
if (IgnoredContextMenuCommands.Contains(commandBuilder.ToString(), StringComparer.OrdinalIgnoreCase)) | |
{ | |
continue; | |
} | |
ImageSource icon = null; | |
if (mii.hbmpItem != IntPtr.Zero) | |
{ | |
icon = GetBitmapSourceFromHBitmap(mii.hbmpItem); | |
} | |
else if (mii.hbmpChecked != IntPtr.Zero) | |
{ | |
icon = GetBitmapSourceFromHBitmap(mii.hbmpChecked); | |
} | |
menuItems.Add(new ContextMenuItem(prefix + menuText, icon, mii.wID)); | |
} | |
} | |
} | |
private static BitmapSource GetBitmapSourceFromHBitmap(IntPtr hBitmap) | |
{ | |
try | |
{ | |
var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap( | |
hBitmap, | |
IntPtr.Zero, | |
Int32Rect.Empty, | |
BitmapSizeOptions.FromWidthAndHeight(16, 16) | |
); | |
if (!DeleteObject(hBitmap)) | |
{ | |
throw new Exception("Failed to delete HBitmap."); | |
} | |
return bitmapSource; | |
} | |
catch (COMException) | |
{ | |
// ignore | |
} | |
return null; | |
} | |
} | |
public static class ShellContextMenuDisplayHelper | |
{ | |
#region DllImport | |
[DllImport("shell32.dll")] | |
private static extern Int32 SHGetMalloc(out IntPtr hObject); | |
[DllImport("shell32.dll")] | |
private static extern Int32 SHParseDisplayName( | |
[MarshalAs(UnmanagedType.LPWStr)] string pszName, | |
IntPtr pbc, | |
out IntPtr ppidl, | |
UInt32 sfgaoIn, | |
out UInt32 psfgaoOut | |
); | |
[DllImport("shell32.dll")] | |
private static extern Int32 SHBindToParent( | |
IntPtr pidl, | |
[MarshalAs(UnmanagedType.LPStruct)] Guid riid, | |
out IntPtr ppv, | |
ref IntPtr ppidlLast | |
); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern IntPtr CreatePopupMenu(); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern bool DestroyMenu(IntPtr hMenu); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern uint GetMenuItemCount(IntPtr hMenu); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern uint GetMenuString( | |
IntPtr hMenu, uint uIDItem, StringBuilder lpString, int nMaxCount, uint uFlag | |
); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern IntPtr GetSubMenu(IntPtr hMenu, int nPos); | |
[DllImport("user32.dll", CharSet = CharSet.Auto)] | |
private static extern bool GetMenuItemInfo(IntPtr hMenu, uint uItem, bool fByPosition, ref MENUITEMINFO lpmii); | |
[DllImport("gdi32.dll")] | |
private static extern bool DeleteObject(IntPtr hObject); | |
#endregion | |
#region Constants | |
private const uint ContextMenuStartId = 0x0001; | |
private const uint ContextMenuEndId = 0x7FFF; | |
private static readonly string[] IgnoredContextMenuCommands = | |
{ | |
// We haven't managed to make these work, so we don't display them in the context menu. | |
"Share", | |
"Windows.ModernShare", | |
"PinToStartScreen", | |
"CopyAsPath", | |
// Hide functionality provided by the Explorer plugin itself | |
"Copy", | |
"Delete" | |
}; | |
#endregion | |
#region Enums | |
[Flags] | |
enum ContextMenuFlags : uint | |
{ | |
Normal = 0x00000000, | |
DefaultOnly = 0x00000001, | |
VerbsOnly = 0x00000002, | |
Explore = 0x00000004, | |
NoVerbs = 0x00000008, | |
CanRename = 0x00000010, | |
NoDefault = 0x00000020, | |
IncludeStatic = 0x00000040, | |
ItemMenu = 0x00000080, | |
ExtendedVerbs = 0x00000100, | |
DisabledVerbs = 0x00000200, | |
AsyncVerbState = 0x00000400, | |
OptimizeForInvoke = 0x00000800, | |
SyncCascadeMenu = 0x00001000, | |
DoNotPickDefault = 0x00002000, | |
Reserved = 0xffff0000 | |
} | |
[Flags] | |
enum ContextMenuInvokeCommandFlags : uint | |
{ | |
Icon = 0x00000010, | |
Hotkey = 0x00000020, | |
FlagNoUi = 0x00000400, | |
Unicode = 0x00004000, | |
NoConsole = 0x00008000, | |
AsyncOk = 0x00100000, | |
NoZoneChecks = 0x00800000, | |
ShiftDown = 0x10000000, | |
ControlDown = 0x40000000, | |
FlagLogUsage = 0x04000000, | |
PointInvoke = 0x20000000 | |
} | |
[Flags] | |
enum MenuItemInformationMask : uint | |
{ | |
Bitmap = 0x00000080, | |
Checkmarks = 0x00000008, | |
Data = 0x00000020, | |
Ftype = 0x00000100, | |
Id = 0x00000002, | |
State = 0x00000001, | |
String = 0x00000040, | |
Submenu = 0x00000004, | |
Type = 0x00000010 | |
} | |
enum MenuItemFtype : uint | |
{ | |
Bitmap = 0x00000004, | |
MenuBarBreak = 0x00000020, | |
MenuBreak = 0x00000040, | |
OwnerDraw = 0x00000100, | |
RadioCheck = 0x00000200, | |
RightJustify = 0x00004000, | |
RightOrder = 0x00002000, | |
Separator = 0x00000800, | |
String = 0x00000000, | |
} | |
enum GetCommandStringFlags : uint | |
{ | |
VerbA = 0x00000000, | |
HelpTextA = 0x00000001, | |
ValidateA = 0x00000002, | |
Unicode = VerbW, | |
Verb = VerbW, | |
VerbW = 0x00000004, | |
HelpText = HelpTextW, | |
HelpTextW = 0x00000005, | |
Validate = ValidateW, | |
ValidateW = 0x00000006, | |
VerbIconW = 0x00000014 | |
} | |
#endregion | |
private static IMalloc GetMalloc() | |
{ | |
SHGetMalloc(out var pMalloc); | |
return (IMalloc)Marshal.GetTypedObjectForIUnknown(pMalloc, typeof(IMalloc)); | |
} | |
public static void ExecuteContextMenuItem(string fileName, uint menuItemId) | |
{ | |
IMalloc malloc = null; | |
IntPtr originalPidl = IntPtr.Zero; | |
IntPtr pShellFolder = IntPtr.Zero; | |
IntPtr pContextMenu = IntPtr.Zero; | |
IntPtr hMenu = IntPtr.Zero; | |
IContextMenu contextMenu = null; | |
IShellFolder shellFolder = null; | |
try | |
{ | |
malloc = GetMalloc(); | |
var hr = SHParseDisplayName(fileName, IntPtr.Zero, out var pidl, 0, out _); | |
if (hr != 0) | |
{ | |
Logger.LogError($"SHParseDisplayName failed with HRESULT: {hr}"); | |
throw new Exception($"SHParseDisplayName failed with HRESULT: {hr}"); | |
} | |
originalPidl = pidl; | |
var guid = typeof(IShellFolder).GUID; | |
hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); | |
if (hr != 0) throw new Exception("SHBindToParent failed"); | |
shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); | |
hr = shellFolder.GetUIObjectOf( | |
IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu | |
); | |
if (hr != 0) throw new Exception("GetUIObjectOf failed"); | |
contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); | |
hMenu = CreatePopupMenu(); | |
contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); | |
var directory = Path.GetDirectoryName(fileName); | |
var invokeCommandInfo = new CMINVOKECOMMANDINFO | |
{ | |
cbSize = (uint)Marshal.SizeOf(typeof(CMINVOKECOMMANDINFO)), | |
fMask = (uint)ContextMenuInvokeCommandFlags.Unicode, | |
hwnd = IntPtr.Zero, | |
lpVerb = (IntPtr)(menuItemId - ContextMenuStartId), | |
lpParameters = null, | |
lpDirectory = null, | |
nShow = 1, | |
hIcon = IntPtr.Zero, | |
}; | |
hr = contextMenu.InvokeCommand(ref invokeCommandInfo); | |
if (hr != 0) | |
{ | |
throw new Exception($"InvokeCommand failed with code {hr:X}"); | |
} | |
} | |
finally | |
{ | |
if (hMenu != IntPtr.Zero) | |
DestroyMenu(hMenu); | |
if (contextMenu != null) | |
Marshal.ReleaseComObject(contextMenu); | |
if (pContextMenu != IntPtr.Zero) | |
Marshal.Release(pContextMenu); | |
if (shellFolder != null) | |
Marshal.ReleaseComObject(shellFolder); | |
if (pShellFolder != IntPtr.Zero) | |
Marshal.Release(pShellFolder); | |
if (originalPidl != IntPtr.Zero) | |
malloc?.Free(originalPidl); | |
if (malloc != null) | |
Marshal.ReleaseComObject(malloc); | |
} | |
} | |
public static List<ContextMenuItem> GetContextMenuWithIcons(string filePath) | |
{ | |
IMalloc malloc = null; | |
IntPtr originalPidl = IntPtr.Zero; | |
IntPtr pShellFolder = IntPtr.Zero; | |
IntPtr pContextMenu = IntPtr.Zero; | |
IntPtr hMenu = IntPtr.Zero; | |
IShellFolder shellFolder = null; | |
IContextMenu contextMenu = null; | |
try | |
{ | |
malloc = GetMalloc(); | |
var hr = SHParseDisplayName(filePath, IntPtr.Zero, out var pidl, 0, out _); | |
if (hr != 0) throw new Exception("SHParseDisplayName failed"); | |
originalPidl = pidl; | |
var guid = typeof(IShellFolder).GUID; | |
hr = SHBindToParent(pidl, guid, out pShellFolder, ref pidl); | |
if (hr != 0) throw new Exception("SHBindToParent failed"); | |
shellFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pShellFolder, typeof(IShellFolder)); | |
hr = shellFolder.GetUIObjectOf( | |
IntPtr.Zero, 1, new[] { pidl }, typeof(IContextMenu).GUID, IntPtr.Zero, out pContextMenu | |
); | |
if (hr != 0) throw new Exception("GetUIObjectOf failed"); | |
contextMenu = (IContextMenu)Marshal.GetTypedObjectForIUnknown(pContextMenu, typeof(IContextMenu)); | |
// Without waiting, some items, such as "Send to > Documents", don't always appear, which shifts item ids | |
// even though it shouldn't. Please replace this if you find a better way to fix this bug. | |
Thread.Sleep(200); | |
hMenu = CreatePopupMenu(); | |
contextMenu.QueryContextMenu(hMenu, 0, ContextMenuStartId, ContextMenuEndId, (uint)ContextMenuFlags.Explore); | |
var menuItems = new List<ContextMenuItem>(); | |
ProcessMenuWithIcons(hMenu, contextMenu, menuItems); | |
return menuItems; | |
} | |
finally | |
{ | |
if (hMenu != IntPtr.Zero) | |
DestroyMenu(hMenu); | |
if (contextMenu != null) | |
Marshal.ReleaseComObject(contextMenu); | |
if (pContextMenu != IntPtr.Zero) | |
Marshal.Release(pContextMenu); | |
if (shellFolder != null) | |
Marshal.ReleaseComObject(shellFolder); | |
if (pShellFolder != IntPtr.Zero) | |
Marshal.Release(pShellFolder); | |
if (originalPidl != IntPtr.Zero) | |
malloc?.Free(originalPidl); | |
if (malloc != null) | |
Marshal.ReleaseComObject(malloc); | |
} | |
} | |
private static void ProcessMenuWithIcons(IntPtr hMenu, IContextMenu contextMenu, List<ContextMenuItem> menuItems, string prefix = "") | |
{ | |
uint menuCount = GetMenuItemCount(hMenu); | |
for (uint i = 0; i < menuCount; i++) | |
{ | |
var mii = new MENUITEMINFO | |
{ | |
cbSize = (uint)Marshal.SizeOf(typeof(MENUITEMINFO)), | |
fMask = (uint)(MenuItemInformationMask.Bitmap | MenuItemInformationMask.Ftype | | |
MenuItemInformationMask.Submenu | MenuItemInformationMask.Id) | |
}; | |
GetMenuItemInfo(hMenu, i, true, ref mii); | |
var menuText = new StringBuilder(256); | |
uint result = GetMenuString(hMenu, mii.wID, menuText, menuText.Capacity, 0); | |
if (result == 0 || string.IsNullOrWhiteSpace(menuText.ToString())) | |
{ | |
continue; | |
} | |
menuText.Replace("&", ""); | |
IntPtr hSubMenu = GetSubMenu(hMenu, (int)i); | |
if (hSubMenu != IntPtr.Zero) | |
{ | |
ProcessMenuWithIcons(hSubMenu, contextMenu, menuItems, prefix + menuText + " > "); | |
} | |
else if (!string.IsNullOrWhiteSpace(menuText.ToString())) | |
{ | |
var commandBuilder = new StringBuilder(256); | |
contextMenu.GetCommandString( | |
mii.wID - ContextMenuStartId, | |
(uint)GetCommandStringFlags.Verb, | |
IntPtr.Zero, | |
commandBuilder, | |
commandBuilder.Capacity | |
); | |
if (IgnoredContextMenuCommands.Contains(commandBuilder.ToString(), StringComparer.OrdinalIgnoreCase)) | |
{ | |
continue; | |
} | |
ImageSource icon = null; | |
if (mii.hbmpItem != IntPtr.Zero) | |
{ | |
icon = GetBitmapSourceFromHBitmap(mii.hbmpItem); | |
} | |
else if (mii.hbmpChecked != IntPtr.Zero) | |
{ | |
icon = GetBitmapSourceFromHBitmap(mii.hbmpChecked); | |
} | |
menuItems.Add(new ContextMenuItem(prefix + menuText, icon, mii.wID)); | |
} | |
} | |
} | |
private static BitmapSource GetBitmapSourceFromHBitmap(IntPtr hBitmap) | |
{ | |
try | |
{ | |
var bitmapSource = Imaging.CreateBitmapSourceFromHBitmap( | |
hBitmap, | |
IntPtr.Zero, | |
Int32Rect.Empty, | |
BitmapSizeOptions.FromWidthAndHeight(16, 16) | |
); | |
if (!DeleteObject(hBitmap)) | |
{ | |
throw new Exception("Failed to delete HBitmap."); | |
} | |
return bitmapSource; | |
} | |
catch (COMException) | |
{ | |
// ignore | |
} | |
return null; | |
} | |
} |
Native context menu has
Send to > Email recipient
option, so fixes #1192?What's the PR
Why do we need a feature
TODO
Combine this with the existingHelper/ShellContextMenu.cs
?Known issues
Pin to Start
returns error code80070005
.Share
returns error code80070578
.Send to
submenuThere might be other commands that don't work.
Test Cases
Summary by CodeRabbit
New Features
Improvements
Settings