Skip to content

Commit a02295a

Browse files
BuraChuhadarBura Chuhadar
andauthored
Bringing custom shellcontextmenu (#699)
* 174: First attempt on windows Explorer context menu * Fixing rebase issues Co-authored-by: Bura Chuhadar <[email protected]>
1 parent 66cc669 commit a02295a

13 files changed

+530
-118
lines changed

Files.Launcher/Files.Launcher.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
<Prefer32Bit>true</Prefer32Bit>
8787
</PropertyGroup>
8888
<ItemGroup>
89+
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
90+
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
91+
</Reference>
8992
<Reference Include="System" />
9093
<Reference Include="System.Core" />
9194
<Reference Include="System.Runtime" />
@@ -138,6 +141,7 @@
138141
<Project>{0533133f-2559-4b53-a0fd-0970bc0e312e}</Project>
139142
<Name>Common</Name>
140143
</ProjectReference>
144+
<None Include="packages.config" />
141145
</ItemGroup>
142146
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
143147
</Project>

Files.Launcher/Program.cs

Lines changed: 152 additions & 102 deletions
Large diffs are not rendered by default.

Files.Launcher/Win32API.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Drawing;
34
using System.Linq;
45
using System.Runtime.InteropServices;
6+
using System.Text;
57
using Vanara.Windows.Shell;
68

79
namespace FilesFullTrust
@@ -84,5 +86,57 @@ public static IList<object> GetFileProperties(ShellItem folderItem, List<(Vanara
8486

8587
return propValueList;
8688
}
89+
90+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
91+
private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
92+
93+
[DllImport("user32.dll", CharSet = CharSet.Auto)]
94+
private static extern int LoadString(IntPtr hInstance, int ID, StringBuilder lpBuffer, int nBufferMax);
95+
96+
[DllImport("kernel32.dll", SetLastError = true)]
97+
[return: MarshalAs(UnmanagedType.Bool)]
98+
private static extern bool FreeLibrary(IntPtr hModule);
99+
100+
101+
public static string ExtractStringFromDLL(string file, int number)
102+
{
103+
IntPtr lib = LoadLibrary(file);
104+
StringBuilder result = new StringBuilder(2048);
105+
LoadString(lib, number, result, result.Capacity);
106+
FreeLibrary(lib);
107+
return result.ToString();
108+
}
109+
110+
111+
[DllImport("shell32.dll", SetLastError = true)]
112+
public static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);
113+
114+
[DllImport("kernel32.dll")]
115+
public static extern IntPtr LocalFree(IntPtr hMem);
116+
117+
public static string[] CommandLineToArgs(string commandLine)
118+
{
119+
if (String.IsNullOrEmpty(commandLine))
120+
return Array.Empty<string>();
121+
122+
var argv = CommandLineToArgvW(commandLine, out int argc);
123+
if (argv == IntPtr.Zero)
124+
throw new System.ComponentModel.Win32Exception();
125+
try
126+
{
127+
var args = new string[argc];
128+
for (var i = 0; i < args.Length; i++)
129+
{
130+
var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
131+
args[i] = Marshal.PtrToStringUni(p);
132+
}
133+
134+
return args;
135+
}
136+
finally
137+
{
138+
Marshal.FreeHGlobal(argv);
139+
}
140+
}
87141
}
88142
}

Files.Launcher/packages.config

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<packages>
3+
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net472" />
4+
</packages>

Files/BaseLayout.cs

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1-
using Files.Filesystem;
1+
using Files.Filesystem;
2+
using Files.Helpers;
23
using Files.Interacts;
34
using Files.View_Models;
45
using Files.Views.Pages;
56
using System;
67
using System.Collections.Generic;
8+
using System.Collections.Immutable;
79
using System.ComponentModel;
810
using System.IO;
911
using System.Linq;
1012
using System.Runtime.CompilerServices;
13+
using System.Threading.Tasks;
1114
using Windows.ApplicationModel.DataTransfer;
1215
using Windows.ApplicationModel.Resources;
1316
using Windows.Storage;
1417
using Windows.System;
1518
using Windows.UI.Core;
1619
using Windows.UI.Xaml;
1720
using Windows.UI.Xaml.Controls;
21+
using Windows.UI.Xaml.Controls.Primitives;
1822
using Windows.UI.Xaml.Input;
23+
using Windows.UI.Xaml.Media;
24+
using Windows.UI.Xaml.Media.Imaging;
1925
using Windows.UI.Xaml.Navigation;
2026

2127
namespace Files
@@ -28,6 +34,7 @@ public abstract class BaseLayout : Page, INotifyPropertyChanged
2834
public SelectedItemsPropertiesViewModel SelectedItemsPropertiesViewModel { get; }
2935
public DirectoryPropertiesViewModel DirectoryPropertiesViewModel { get; }
3036
public bool IsQuickLookEnabled { get; set; } = false;
37+
public MenuFlyout BaseLayoutItemContextFlyout { get; set; }
3138

3239
public ItemViewModel AssociatedViewModel = null;
3340
public Interaction AssociatedInteractions = null;
@@ -111,10 +118,6 @@ public BaseLayout()
111118
}
112119
}
113120

114-
public abstract void SetSelectedItemOnUi(ListedItem item);
115-
116-
public abstract void SetSelectedItemsOnUi(List<ListedItem> items);
117-
118121
public abstract void SelectAllItems();
119122

120123
public abstract void InvertSelection();
@@ -127,6 +130,42 @@ public BaseLayout()
127130

128131
public abstract int GetSelectedIndex();
129132

133+
public abstract void SetSelectedItemOnUi(ListedItem selectedItem);
134+
public abstract void SetSelectedItemsOnUi(List<ListedItem> selectedItems);
135+
136+
private void ClearShellContextMenus()
137+
{
138+
var contextMenuItems = BaseLayoutItemContextFlyout.Items.Where(c => c.Tag != null && ParseContextMenuTag(c.Tag).commandKey != null).ToList();
139+
for (int i = 0; i < contextMenuItems.Count; i++)
140+
{
141+
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(contextMenuItems[i]));
142+
}
143+
if (BaseLayoutItemContextFlyout.Items[0] is MenuFlyoutSeparator flyoutSeperator)
144+
{
145+
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(flyoutSeperator));
146+
}
147+
}
148+
149+
public virtual void SetShellContextmenu()
150+
{
151+
ClearShellContextMenus();
152+
if (_SelectedItems != null && _SelectedItems.Count > 0)
153+
{
154+
var currentBaseLayoutItemCount = BaseLayoutItemContextFlyout.Items.Count;
155+
var isDirectory = !_SelectedItems.Any(c=> c.PrimaryItemAttribute == StorageItemTypes.File || c.PrimaryItemAttribute == StorageItemTypes.None);
156+
foreach (var selectedItem in _SelectedItems)
157+
{
158+
var menuFlyoutItems = Task.Run(() => new RegistryReader().GetExtensionContextMenuForFiles(isDirectory, selectedItem.FileExtension));
159+
LoadMenuFlyoutItem(menuFlyoutItems.Result);
160+
}
161+
var totalFlyoutItems = BaseLayoutItemContextFlyout.Items.Count - currentBaseLayoutItemCount;
162+
if (totalFlyoutItems > 0 && !(BaseLayoutItemContextFlyout.Items[totalFlyoutItems] is MenuFlyoutSeparator))
163+
{
164+
BaseLayoutItemContextFlyout.Items.Insert(totalFlyoutItems, new MenuFlyoutSeparator());
165+
}
166+
}
167+
}
168+
130169
public abstract void FocusSelectedItems();
131170

132171
public abstract void StartRenameItem();
@@ -209,8 +248,71 @@ private void UnloadMenuFlyoutItemByName(string nameToUnload)
209248
(menuItem as MenuFlyoutItemBase).Visibility = Visibility.Collapsed;
210249
}
211250

251+
private void LoadMenuFlyoutItem(IEnumerable<(string commandKey,string commandName, string commandIcon, string command)> menuFlyoutItems)
252+
{
253+
foreach (var menuFlyoutItem in menuFlyoutItems)
254+
{
255+
if (BaseLayoutItemContextFlyout.Items.Any(c => ParseContextMenuTag(c.Tag).commandKey == menuFlyoutItem.commandKey))
256+
{
257+
continue;
258+
}
259+
260+
var menuLayoutItem = new MenuFlyoutItem()
261+
{
262+
Text = menuFlyoutItem.commandName,
263+
Tag = menuFlyoutItem
264+
};
265+
menuLayoutItem.Click += MenuLayoutItem_Click;
266+
267+
BaseLayoutItemContextFlyout.Items.Insert(0, menuLayoutItem);
268+
}
269+
}
270+
271+
private (string commandKey, string commandName, string commandIcon, string command) ParseContextMenuTag(object tag)
272+
{
273+
if(tag is ValueTuple<string, string, string, string>)
274+
{
275+
(string commandKey, string commandName, string commandIcon, string command) = (ValueTuple<string, string, string, string>)tag;
276+
return (commandKey, commandName, commandIcon, command);
277+
}
278+
279+
return (null, null, null, null);
280+
}
281+
282+
private async void MenuLayoutItem_Click(object sender, RoutedEventArgs e)
283+
{
284+
var selectedFileSystemItems = (App.CurrentInstance.ContentPage as BaseLayout).SelectedItems;
285+
var currentMenuLayoutItem = (MenuFlyoutItem)sender;
286+
if (currentMenuLayoutItem != null)
287+
{
288+
var (_, _, _, command) = ParseContextMenuTag(currentMenuLayoutItem.Tag);
289+
if (selectedFileSystemItems.Count > 1)
290+
{
291+
foreach (var selectedDataItem in selectedFileSystemItems)
292+
{
293+
var commandToExecute = await new ShellCommandParser().ParseShellCommand(command, selectedDataItem.ItemPath);
294+
if (!string.IsNullOrEmpty(commandToExecute.command))
295+
{
296+
await Interaction.InvokeWin32Component(commandToExecute.command, commandToExecute.arguments);
297+
}
298+
}
299+
}
300+
else if (selectedFileSystemItems.Count == 1)
301+
{
302+
var selectedDataItem = selectedFileSystemItems[0] as ListedItem;
303+
304+
var commandToExecute = await new ShellCommandParser().ParseShellCommand(command, selectedDataItem.ItemPath);
305+
if (!string.IsNullOrEmpty(commandToExecute.command))
306+
{
307+
await Interaction.InvokeWin32Component(commandToExecute.command, commandToExecute.arguments);
308+
}
309+
}
310+
}
311+
}
312+
212313
public void RightClickContextMenu_Opening(object sender, object e)
213314
{
315+
SetShellContextmenu();
214316
if (App.CurrentInstance.FilesystemViewModel.WorkingDirectory.StartsWith(App.AppSettings.RecycleBinPath))
215317
{
216318
(this.FindName("EmptyRecycleBin") as MenuFlyoutItemBase).Visibility = Visibility.Visible;

Files/Files.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,12 @@
167167
<Compile Include="Helpers\NativeFindStorageItemHelper.cs" />
168168
<Compile Include="Helpers\NaturalStringComparer.cs" />
169169
<Compile Include="Helpers\PackageHelper.cs" />
170+
<Compile Include="Helpers\ShellCommandParser.cs" />
170171
<Compile Include="Helpers\StringExtensions.cs" />
171172
<Compile Include="Helpers\BulkObservableCollection.cs" />
172173
<Compile Include="Helpers\ThemeHelper.cs" />
173174
<Compile Include="Program.cs" />
175+
<Compile Include="Helpers\RegistryReader.cs" />
174176
<Compile Include="ResourceController.cs" />
175177
<Compile Include="INavigationToolbar.cs" />
176178
<Compile Include="UserControls\ModernNavigationToolbar.xaml.cs">

Files/Helpers/RegistryReader.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using Microsoft.Win32;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Runtime.InteropServices.WindowsRuntime;
7+
using System.Security.Principal;
8+
using System.Text;
9+
using System.Threading.Tasks;
10+
using Windows.Foundation.Collections;
11+
using Windows.Graphics.Imaging;
12+
using Windows.UI.Xaml.Controls;
13+
using Windows.UI.Xaml.Media.Imaging;
14+
15+
namespace Files.Helpers
16+
{
17+
class RegistryReader
18+
{
19+
20+
private async Task ParseRegistryAndAddToList(List<(string commandKey, string commandName, string commandIcon, string command)> shellList, RegistryKey shellKey)
21+
{
22+
if(shellKey != null)
23+
{
24+
foreach (var keyname in shellKey.GetSubKeyNames())
25+
{
26+
try
27+
{
28+
var commandNameKey = shellKey.OpenSubKey(keyname);
29+
var commandName = commandNameKey.GetValue(String.Empty)?.ToString() ?? "";
30+
//@ is a special command under the registry. We need to search for MUIVerb:
31+
if (string.IsNullOrEmpty(commandName) || commandName.StartsWith("@"))
32+
{
33+
34+
var muiVerb = commandNameKey.GetValue("MUIVerb")?.ToString() ?? "";
35+
if (!string.IsNullOrEmpty(muiVerb) && App.Connection != null)
36+
{
37+
var muiVerbRequest = new ValueSet
38+
{
39+
{ "Arguments", "LoadMUIVerb" },
40+
{ "MUIVerbLocation", muiVerb?.Split(',')[0]?.TrimStart('@') },
41+
{ "MUIVerbLine", Convert.ToInt32(muiVerb?.Split(',')[1]?.TrimStart('-')) }
42+
};
43+
var responseMUIVerb = await App.Connection.SendMessageAsync(muiVerbRequest);
44+
if (responseMUIVerb.Status == Windows.ApplicationModel.AppService.AppServiceResponseStatus.Success
45+
&& responseMUIVerb.Message.ContainsKey("MUIVerbString"))
46+
{
47+
commandName = (string)responseMUIVerb.Message["MUIVerbString"];
48+
if (string.IsNullOrEmpty(commandName))
49+
{
50+
continue;
51+
}
52+
}
53+
}
54+
else
55+
{
56+
continue;
57+
}
58+
}
59+
var commandNameString = commandName.Replace("&", "");
60+
var commandIconString = commandNameKey.GetValue("Icon")?.ToString();
61+
62+
63+
var commandNameKeyNames = commandNameKey.GetSubKeyNames();
64+
if (commandNameKeyNames.Contains("command") && !shellList.Any(c => c.commandKey == keyname))
65+
{
66+
var command = commandNameKey.OpenSubKey("command");
67+
shellList.Add((commandKey: keyname, commandNameString, commandIconString, command: command.GetValue(string.Empty).ToString()));
68+
69+
}
70+
}
71+
catch
72+
{
73+
continue;
74+
}
75+
76+
}
77+
}
78+
}
79+
80+
public async Task<IEnumerable<(string commanyKey, string commandName, string commandIcon, string command)>> GetExtensionContextMenuForFiles(bool isDirectory, string fileExtension)
81+
{
82+
83+
var shellList = new List<(string commandKey, string commandName, string commandIcon, string command)>();
84+
try
85+
{
86+
87+
if(isDirectory)
88+
{
89+
using RegistryKey classRootDirectoryShellKey = Registry.ClassesRoot.OpenSubKey("Directory\\shell");
90+
await ParseRegistryAndAddToList(shellList, classRootDirectoryShellKey);
91+
}
92+
else
93+
{
94+
using RegistryKey classRootShellKey = Registry.ClassesRoot.OpenSubKey("*\\shell");
95+
await ParseRegistryAndAddToList(shellList, classRootShellKey);
96+
97+
using RegistryKey classRootFileExtensionShellKey = Registry.ClassesRoot.OpenSubKey($"{fileExtension}\\shell");
98+
await ParseRegistryAndAddToList(shellList, classRootFileExtensionShellKey);
99+
100+
using RegistryKey currentUserShellKey = Registry.CurrentUser.OpenSubKey("Software\\Classes\\*\\shell");
101+
await ParseRegistryAndAddToList(shellList, currentUserShellKey);
102+
103+
using RegistryKey currentUserFileExtensionShellKey = Registry.CurrentUser.OpenSubKey($"Software\\Classes\\{fileExtension}\\shell");
104+
await ParseRegistryAndAddToList(shellList, currentUserFileExtensionShellKey);
105+
106+
}
107+
108+
return shellList;
109+
}
110+
catch
111+
{
112+
return shellList;
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)