Skip to content

Commit 6b6878c

Browse files
gave92yaira2
andauthored
Added support for Dynamic Shell Context Menus (#1754)
Co-authored-by: Yair Aichenbaum <[email protected]>
1 parent 5b5cb85 commit 6b6878c

33 files changed

+1463
-383
lines changed

Common/ContextMenu.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections.Generic;
2+
3+
namespace Files.Common
4+
{
5+
// Same definition of Vanara.PInvoke.User32.MenuItemType
6+
public enum MenuItemType : uint
7+
{
8+
MFT_STRING = 0,
9+
MFT_BITMAP = 4,
10+
MFT_MENUBARBREAK = 32,
11+
MFT_MENUBREAK = 64,
12+
MFT_OWNERDRAW = 256,
13+
MFT_RADIOCHECK = 512,
14+
MFT_SEPARATOR = 2048,
15+
MFT_RIGHTORDER = 8192,
16+
MFT_RIGHTJUSTIFY = 16384
17+
}
18+
19+
public class Win32ContextMenu
20+
{
21+
public List<Win32ContextMenuItem> Items { get; set; }
22+
}
23+
24+
public class Win32ContextMenuItem
25+
{
26+
public string IconBase64 { get; set; }
27+
public int ID { get; set; } // Valid only in current menu to invoke item
28+
public string Label { get; set; }
29+
public string CommandString { get; set; }
30+
public MenuItemType Type { get; set; }
31+
public List<Win32ContextMenuItem> SubItems { get; set; }
32+
}
33+
}

Files.Launcher/Files.Launcher.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
<Compile Include="Properties\AssemblyInfo.cs" />
110110
<Compile Include="QuickLook.cs" />
111111
<Compile Include="Win32API.cs" />
112+
<Compile Include="Win32API_ContextMenu.cs" />
112113
</ItemGroup>
113114
<ItemGroup>
114115
<None Include="app.config" />

Files.Launcher/Program.cs

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.Text.RegularExpressions;
1212
using System.Threading;
1313
using System.Threading.Tasks;
14+
using System.Windows.Forms;
1415
using Vanara.PInvoke;
1516
using Vanara.Windows.Shell;
1617
using Windows.ApplicationModel;
@@ -50,6 +51,9 @@ private static void Main(string[] args)
5051

5152
try
5253
{
54+
// Create handle table to store e.g. context menu references
55+
handleTable = new Win32API.DisposableDictionary();
56+
5357
// Create shell COM object and get recycle bin folder
5458
recycler = new ShellFolder(Shell32.KNOWNFOLDERID.FOLDERID_RecycleBinFolder);
5559
ApplicationData.Current.LocalSettings.Values["RecycleBin_Title"] = recycler.Name;
@@ -89,6 +93,7 @@ private static void Main(string[] args)
8993
{
9094
watcher.Dispose();
9195
}
96+
handleTable?.Dispose();
9297
recycler?.Dispose();
9398
appServiceExit?.Dispose();
9499
mutex?.ReleaseMutex();
@@ -129,6 +134,7 @@ private static async void Watcher_Changed(object sender, FileSystemEventArgs e)
129134
private static AppServiceConnection connection;
130135
private static AutoResetEvent appServiceExit;
131136
private static ShellFolder recycler;
137+
private static Win32API.DisposableDictionary handleTable;
132138
private static IList<FileSystemWatcher> watchers;
133139

134140
private static async void InitializeAppServiceConnection()
@@ -233,17 +239,36 @@ private static async Task parseArguments(AppServiceRequestReceivedEventArgs args
233239
process.Start();
234240
break;
235241

236-
case "LoadMUIVerb":
237-
var responseSet = new ValueSet();
238-
responseSet.Add("MUIVerbString", Win32API.ExtractStringFromDLL((string)args.Request.Message["MUIVerbLocation"], (int)args.Request.Message["MUIVerbLine"]));
239-
await args.Request.SendResponseAsync(responseSet);
242+
case "LoadContextMenu":
243+
var contextMenuResponse = new ValueSet();
244+
var loadThreadWithMessageQueue = new Win32API.ThreadWithMessageQueue<ValueSet>(HandleMenuMessage);
245+
var cMenuLoad = await loadThreadWithMessageQueue.PostMessage<Win32API.ContextMenu>(args.Request.Message);
246+
contextMenuResponse.Add("Handle", handleTable.AddValue(loadThreadWithMessageQueue));
247+
contextMenuResponse.Add("ContextMenu", JsonConvert.SerializeObject(cMenuLoad));
248+
await args.Request.SendResponseAsync(contextMenuResponse);
240249
break;
241250

242-
case "ParseAguments":
243-
var responseArray = new ValueSet();
244-
var resultArgument = Win32API.CommandLineToArgs((string)args.Request.Message["Command"]);
245-
responseArray.Add("ParsedArguments", JsonConvert.SerializeObject(resultArgument));
246-
await args.Request.SendResponseAsync(responseArray);
251+
case "ExecAndCloseContextMenu":
252+
var menuKey = (string)args.Request.Message["Handle"];
253+
var execThreadWithMessageQueue = handleTable.GetValue<Win32API.ThreadWithMessageQueue<ValueSet>>(menuKey);
254+
if (execThreadWithMessageQueue != null)
255+
{
256+
await execThreadWithMessageQueue.PostMessage(args.Request.Message);
257+
}
258+
// The following line is needed to cleanup resources when menu is closed.
259+
// Unfortunately if you uncomment it some menu items will randomly stop working.
260+
// Resource cleanup is currently done on app closing,
261+
// if we find a solution for the issue above, we should cleanup as soon as a menu is closed.
262+
//handleTable.RemoveValue(menuKey);
263+
break;
264+
265+
case "InvokeVerb":
266+
var filePath = (string)args.Request.Message["FilePath"];
267+
var split = filePath.Split('|').Where(x => !string.IsNullOrWhiteSpace(x));
268+
using (var cMenu = Win32API.ContextMenu.GetContextMenuForFiles(split.ToArray(), Shell32.CMF.CMF_DEFAULTONLY))
269+
{
270+
cMenu?.InvokeVerb((string)args.Request.Message["Verb"]);
271+
}
247272
break;
248273

249274
case "Bitlocker":
@@ -276,6 +301,56 @@ private static async Task parseArguments(AppServiceRequestReceivedEventArgs args
276301
}
277302
}
278303

304+
private static object HandleMenuMessage(ValueSet message, Win32API.DisposableDictionary table)
305+
{
306+
switch ((string)message["Arguments"])
307+
{
308+
case "LoadContextMenu":
309+
var contextMenuResponse = new ValueSet();
310+
var filePath = (string)message["FilePath"];
311+
var extendedMenu = (bool)message["ExtendedMenu"];
312+
var showOpenMenu = (bool)message["ShowOpenMenu"];
313+
var split = filePath.Split('|').Where(x => !string.IsNullOrWhiteSpace(x));
314+
var cMenuLoad = Win32API.ContextMenu.GetContextMenuForFiles(split.ToArray(),
315+
extendedMenu ? Shell32.CMF.CMF_EXTENDEDVERBS : Shell32.CMF.CMF_NORMAL, FilterMenuItems(showOpenMenu));
316+
table.SetValue("MENU", cMenuLoad);
317+
return cMenuLoad;
318+
319+
case "ExecAndCloseContextMenu":
320+
var cMenuExec = table.GetValue<Win32API.ContextMenu>("MENU");
321+
cMenuExec?.InvokeItem(message.Get("ItemID", -1));
322+
// The following line is needed to cleanup resources when menu is closed.
323+
// Unfortunately if you uncomment it some menu items will randomly stop working.
324+
// Resource cleanup is currently done on app closing,
325+
// if we find a solution for the issue above, we should cleanup as soon as a menu is closed.
326+
//table.RemoveValue("MENU");
327+
return null;
328+
329+
default:
330+
return null;
331+
}
332+
}
333+
334+
private static Func<string, bool> FilterMenuItems(bool showOpenMenu)
335+
{
336+
var knownItems = new List<string>() {
337+
"opennew", "openas", "opencontaining", "opennewprocess",
338+
"runas", "runasuser", "pintohome",
339+
"cut", "copy", "delete", "properties", "link",
340+
"WSL", "Windows.ModernShare", "Windows.Share", "setdesktopwallpaper",
341+
Win32API.ExtractStringFromDLL("shell32.dll", 30312), // SendTo menu
342+
Win32API.ExtractStringFromDLL("shell32.dll", 34593), // Add to collection
343+
};
344+
345+
bool filterMenuItemsImpl(string menuItem)
346+
{
347+
return string.IsNullOrEmpty(menuItem) ? false : knownItems.Contains(menuItem)
348+
|| (!showOpenMenu && menuItem.Equals("open", StringComparison.OrdinalIgnoreCase));
349+
}
350+
351+
return filterMenuItemsImpl;
352+
}
353+
279354
private static async Task parseFileOperation(AppServiceRequestReceivedEventArgs args)
280355
{
281356
var fileOp = (string)args.Request.Message["fileop"];
@@ -552,32 +627,8 @@ await Win32API.StartSTATask(() =>
552627
{
553628
continue;
554629
}
555-
556-
var files = group.Select(x => new ShellItem(x));
557-
using var sf = files.First().Parent;
558-
Shell32.IContextMenu menu = null;
559-
try
560-
{
561-
menu = sf.GetChildrenUIObjects<Shell32.IContextMenu>(null, files.ToArray());
562-
menu.QueryContextMenu(HMENU.NULL, 0, 0, 0, Shell32.CMF.CMF_DEFAULTONLY);
563-
var pici = new Shell32.CMINVOKECOMMANDINFOEX();
564-
pici.lpVerb = new SafeResourceId(Shell32.CMDSTR_OPEN, CharSet.Ansi);
565-
pici.nShow = ShowWindowCommand.SW_SHOW;
566-
pici.cbSize = (uint)Marshal.SizeOf(pici);
567-
menu.InvokeCommand(pici);
568-
}
569-
finally
570-
{
571-
foreach (var elem in files)
572-
{
573-
elem.Dispose();
574-
}
575-
576-
if (menu != null)
577-
{
578-
Marshal.ReleaseComObject(menu);
579-
}
580-
}
630+
using var cMenu = Win32API.ContextMenu.GetContextMenuForFiles(group.ToArray(), Shell32.CMF.CMF_DEFAULTONLY);
631+
cMenu?.InvokeVerb(Shell32.CMDSTR_OPEN);
581632
}
582633
}
583634
return true;

Files.Launcher/Win32API.cs

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.ComponentModel;
43
using System.Diagnostics;
54
using System.IO;
@@ -14,7 +13,7 @@
1413

1514
namespace FilesFullTrust
1615
{
17-
internal class Win32API
16+
internal partial class Win32API
1817
{
1918
public static Task<T> StartSTATask<T>(Func<T> func)
2019
{
@@ -55,55 +54,6 @@ public static async Task<string> GetFileAssociation(string filename)
5554
return null;
5655
}
5756

58-
public enum PropertyReturnType
59-
{
60-
RAWVALUE,
61-
DISPLAYVALUE
62-
}
63-
64-
public static List<(Ole32.PROPERTYKEY propertyKey, PropertyReturnType returnType)> RecyledFileProperties =
65-
new List<(Ole32.PROPERTYKEY propertyKey, PropertyReturnType returnType)>
66-
{
67-
(Ole32.PROPERTYKEY.System.Size, PropertyReturnType.RAWVALUE),
68-
(Ole32.PROPERTYKEY.System.Size, PropertyReturnType.DISPLAYVALUE),
69-
(Ole32.PROPERTYKEY.System.ItemTypeText, PropertyReturnType.RAWVALUE),
70-
(PropertyStore.GetPropertyKeyFromName("System.Recycle.DateDeleted"), PropertyReturnType.RAWVALUE)
71-
};
72-
73-
// A faster method of getting file shell properties (currently non used)
74-
public static IList<object> GetFileProperties(ShellItem folderItem, List<(Ole32.PROPERTYKEY propertyKey, PropertyReturnType returnType)> properties)
75-
{
76-
var propValueList = new List<object>(properties.Count);
77-
var flags = PropSys.GETPROPERTYSTOREFLAGS.GPS_DEFAULT | PropSys.GETPROPERTYSTOREFLAGS.GPS_FASTPROPERTIESONLY;
78-
79-
PropSys.IPropertyStore pStore = null;
80-
try
81-
{
82-
pStore = ((Shell32.IShellItem2)folderItem.IShellItem).GetPropertyStoreForKeys(properties.Select(p => p.propertyKey).ToArray(), (uint)properties.Count, flags, typeof(PropSys.IPropertyStore).GUID);
83-
foreach (var prop in properties)
84-
{
85-
using var propVariant = new Ole32.PROPVARIANT();
86-
pStore.GetValue(prop.propertyKey, propVariant);
87-
if (prop.returnType == PropertyReturnType.RAWVALUE)
88-
{
89-
propValueList.Add(propVariant.Value);
90-
}
91-
else if (prop.returnType == PropertyReturnType.DISPLAYVALUE)
92-
{
93-
using var pDesc = PropertyDescription.Create(prop.propertyKey);
94-
var pValue = pDesc?.FormatForDisplay(propVariant, PropSys.PROPDESC_FORMAT_FLAGS.PDFF_DEFAULT);
95-
propValueList.Add(pValue);
96-
}
97-
}
98-
}
99-
finally
100-
{
101-
Marshal.ReleaseComObject(pStore);
102-
}
103-
104-
return propValueList;
105-
}
106-
10757
public static string ExtractStringFromDLL(string file, int number)
10858
{
10959
var lib = Kernel32.LoadLibrary(file);
@@ -163,4 +113,11 @@ public static void UnlockBitlockerDrive(string drive, string password)
163113
}
164114
}
165115
}
116+
117+
// There is usually no need to define Win32 COM interfaces/P-Invoke methods here.
118+
// The Vanara library contains the definitions for all members of Shell32.dll, User32.dll and more
119+
// The ones below are due to bugs in the current version of the library and can be removed once fixed
120+
#region WIN32_INTERFACES
121+
122+
#endregion
166123
}

0 commit comments

Comments
 (0)