Skip to content

Bringing custom shellcontextmenu #699

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

Merged
merged 2 commits into from
Jun 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Files.Launcher/Files.Launcher.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
<Prefer32Bit>true</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime" />
Expand Down Expand Up @@ -138,6 +141,7 @@
<Project>{0533133f-2559-4b53-a0fd-0970bc0e312e}</Project>
<Name>Common</Name>
</ProjectReference>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
254 changes: 152 additions & 102 deletions Files.Launcher/Program.cs

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions Files.Launcher/Win32API.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Vanara.Windows.Shell;

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

return propValueList;
}

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
private static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int LoadString(IntPtr hInstance, int ID, StringBuilder lpBuffer, int nBufferMax);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);


public static string ExtractStringFromDLL(string file, int number)
{
IntPtr lib = LoadLibrary(file);
StringBuilder result = new StringBuilder(2048);
LoadString(lib, number, result, result.Capacity);
FreeLibrary(lib);
return result.ToString();
}


[DllImport("shell32.dll", SetLastError = true)]
public static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs);

[DllImport("kernel32.dll")]
public static extern IntPtr LocalFree(IntPtr hMem);

public static string[] CommandLineToArgs(string commandLine)
{
if (String.IsNullOrEmpty(commandLine))
return Array.Empty<string>();

var argv = CommandLineToArgvW(commandLine, out int argc);
if (argv == IntPtr.Zero)
throw new System.ComponentModel.Win32Exception();
try
{
var args = new string[argc];
for (var i = 0; i < args.Length; i++)
{
var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
args[i] = Marshal.PtrToStringUni(p);
}

return args;
}
finally
{
Marshal.FreeHGlobal(argv);
}
}
}
}
4 changes: 4 additions & 0 deletions Files.Launcher/packages.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net472" />
</packages>
112 changes: 107 additions & 5 deletions Files/BaseLayout.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
using Files.Filesystem;
using Files.Filesystem;
using Files.Helpers;
using Files.Interacts;
using Files.View_Models;
using Files.Views.Pages;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
using Windows.ApplicationModel.Resources;
using Windows.Storage;
using Windows.System;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Navigation;

namespace Files
Expand All @@ -28,6 +34,7 @@ public abstract class BaseLayout : Page, INotifyPropertyChanged
public SelectedItemsPropertiesViewModel SelectedItemsPropertiesViewModel { get; }
public DirectoryPropertiesViewModel DirectoryPropertiesViewModel { get; }
public bool IsQuickLookEnabled { get; set; } = false;
public MenuFlyout BaseLayoutItemContextFlyout { get; set; }

public ItemViewModel AssociatedViewModel = null;
public Interaction AssociatedInteractions = null;
Expand Down Expand Up @@ -111,10 +118,6 @@ public BaseLayout()
}
}

public abstract void SetSelectedItemOnUi(ListedItem item);

public abstract void SetSelectedItemsOnUi(List<ListedItem> items);

public abstract void SelectAllItems();

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

public abstract int GetSelectedIndex();

public abstract void SetSelectedItemOnUi(ListedItem selectedItem);
public abstract void SetSelectedItemsOnUi(List<ListedItem> selectedItems);

private void ClearShellContextMenus()
{
var contextMenuItems = BaseLayoutItemContextFlyout.Items.Where(c => c.Tag != null && ParseContextMenuTag(c.Tag).commandKey != null).ToList();
for (int i = 0; i < contextMenuItems.Count; i++)
{
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(contextMenuItems[i]));
}
if (BaseLayoutItemContextFlyout.Items[0] is MenuFlyoutSeparator flyoutSeperator)
{
BaseLayoutItemContextFlyout.Items.RemoveAt(BaseLayoutItemContextFlyout.Items.IndexOf(flyoutSeperator));
}
}

public virtual void SetShellContextmenu()
{
ClearShellContextMenus();
if (_SelectedItems != null && _SelectedItems.Count > 0)
{
var currentBaseLayoutItemCount = BaseLayoutItemContextFlyout.Items.Count;
var isDirectory = !_SelectedItems.Any(c=> c.PrimaryItemAttribute == StorageItemTypes.File || c.PrimaryItemAttribute == StorageItemTypes.None);
foreach (var selectedItem in _SelectedItems)
{
var menuFlyoutItems = Task.Run(() => new RegistryReader().GetExtensionContextMenuForFiles(isDirectory, selectedItem.FileExtension));
LoadMenuFlyoutItem(menuFlyoutItems.Result);
}
var totalFlyoutItems = BaseLayoutItemContextFlyout.Items.Count - currentBaseLayoutItemCount;
if (totalFlyoutItems > 0 && !(BaseLayoutItemContextFlyout.Items[totalFlyoutItems] is MenuFlyoutSeparator))
{
BaseLayoutItemContextFlyout.Items.Insert(totalFlyoutItems, new MenuFlyoutSeparator());
}
}
}

public abstract void FocusSelectedItems();

public abstract void StartRenameItem();
Expand Down Expand Up @@ -209,8 +248,71 @@ private void UnloadMenuFlyoutItemByName(string nameToUnload)
(menuItem as MenuFlyoutItemBase).Visibility = Visibility.Collapsed;
}

private void LoadMenuFlyoutItem(IEnumerable<(string commandKey,string commandName, string commandIcon, string command)> menuFlyoutItems)
{
foreach (var menuFlyoutItem in menuFlyoutItems)
{
if (BaseLayoutItemContextFlyout.Items.Any(c => ParseContextMenuTag(c.Tag).commandKey == menuFlyoutItem.commandKey))
{
continue;
}

var menuLayoutItem = new MenuFlyoutItem()
{
Text = menuFlyoutItem.commandName,
Tag = menuFlyoutItem
};
menuLayoutItem.Click += MenuLayoutItem_Click;

BaseLayoutItemContextFlyout.Items.Insert(0, menuLayoutItem);
}
}

private (string commandKey, string commandName, string commandIcon, string command) ParseContextMenuTag(object tag)
{
if(tag is ValueTuple<string, string, string, string>)
{
(string commandKey, string commandName, string commandIcon, string command) = (ValueTuple<string, string, string, string>)tag;
return (commandKey, commandName, commandIcon, command);
}

return (null, null, null, null);
}

private async void MenuLayoutItem_Click(object sender, RoutedEventArgs e)
{
var selectedFileSystemItems = (App.CurrentInstance.ContentPage as BaseLayout).SelectedItems;
var currentMenuLayoutItem = (MenuFlyoutItem)sender;
if (currentMenuLayoutItem != null)
{
var (_, _, _, command) = ParseContextMenuTag(currentMenuLayoutItem.Tag);
if (selectedFileSystemItems.Count > 1)
{
foreach (var selectedDataItem in selectedFileSystemItems)
{
var commandToExecute = await new ShellCommandParser().ParseShellCommand(command, selectedDataItem.ItemPath);
if (!string.IsNullOrEmpty(commandToExecute.command))
{
await Interaction.InvokeWin32Component(commandToExecute.command, commandToExecute.arguments);
}
}
}
else if (selectedFileSystemItems.Count == 1)
{
var selectedDataItem = selectedFileSystemItems[0] as ListedItem;

var commandToExecute = await new ShellCommandParser().ParseShellCommand(command, selectedDataItem.ItemPath);
if (!string.IsNullOrEmpty(commandToExecute.command))
{
await Interaction.InvokeWin32Component(commandToExecute.command, commandToExecute.arguments);
}
}
}
}

public void RightClickContextMenu_Opening(object sender, object e)
{
SetShellContextmenu();
if (App.CurrentInstance.FilesystemViewModel.WorkingDirectory.StartsWith(App.AppSettings.RecycleBinPath))
{
(this.FindName("EmptyRecycleBin") as MenuFlyoutItemBase).Visibility = Visibility.Visible;
Expand Down
2 changes: 2 additions & 0 deletions Files/Files.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,12 @@
<Compile Include="Helpers\NativeFindStorageItemHelper.cs" />
<Compile Include="Helpers\NaturalStringComparer.cs" />
<Compile Include="Helpers\PackageHelper.cs" />
<Compile Include="Helpers\ShellCommandParser.cs" />
<Compile Include="Helpers\StringExtensions.cs" />
<Compile Include="Helpers\BulkObservableCollection.cs" />
<Compile Include="Helpers\ThemeHelper.cs" />
<Compile Include="Program.cs" />
<Compile Include="Helpers\RegistryReader.cs" />
<Compile Include="ResourceController.cs" />
<Compile Include="INavigationToolbar.cs" />
<Compile Include="UserControls\ModernNavigationToolbar.xaml.cs">
Expand Down
116 changes: 116 additions & 0 deletions Files/Helpers/RegistryReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation.Collections;
using Windows.Graphics.Imaging;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;

namespace Files.Helpers
{
class RegistryReader
{

private async Task ParseRegistryAndAddToList(List<(string commandKey, string commandName, string commandIcon, string command)> shellList, RegistryKey shellKey)
{
if(shellKey != null)
{
foreach (var keyname in shellKey.GetSubKeyNames())
{
try
{
var commandNameKey = shellKey.OpenSubKey(keyname);
var commandName = commandNameKey.GetValue(String.Empty)?.ToString() ?? "";
//@ is a special command under the registry. We need to search for MUIVerb:
if (string.IsNullOrEmpty(commandName) || commandName.StartsWith("@"))
{

var muiVerb = commandNameKey.GetValue("MUIVerb")?.ToString() ?? "";
if (!string.IsNullOrEmpty(muiVerb) && App.Connection != null)
{
var muiVerbRequest = new ValueSet
{
{ "Arguments", "LoadMUIVerb" },
{ "MUIVerbLocation", muiVerb?.Split(',')[0]?.TrimStart('@') },
{ "MUIVerbLine", Convert.ToInt32(muiVerb?.Split(',')[1]?.TrimStart('-')) }
};
var responseMUIVerb = await App.Connection.SendMessageAsync(muiVerbRequest);
if (responseMUIVerb.Status == Windows.ApplicationModel.AppService.AppServiceResponseStatus.Success
&& responseMUIVerb.Message.ContainsKey("MUIVerbString"))
{
commandName = (string)responseMUIVerb.Message["MUIVerbString"];
if (string.IsNullOrEmpty(commandName))
{
continue;
}
}
}
else
{
continue;
}
}
var commandNameString = commandName.Replace("&", "");
var commandIconString = commandNameKey.GetValue("Icon")?.ToString();


var commandNameKeyNames = commandNameKey.GetSubKeyNames();
if (commandNameKeyNames.Contains("command") && !shellList.Any(c => c.commandKey == keyname))
{
var command = commandNameKey.OpenSubKey("command");
shellList.Add((commandKey: keyname, commandNameString, commandIconString, command: command.GetValue(string.Empty).ToString()));

}
}
catch
{
continue;
}

}
}
}

public async Task<IEnumerable<(string commanyKey, string commandName, string commandIcon, string command)>> GetExtensionContextMenuForFiles(bool isDirectory, string fileExtension)
{

var shellList = new List<(string commandKey, string commandName, string commandIcon, string command)>();
try
{

if(isDirectory)
{
using RegistryKey classRootDirectoryShellKey = Registry.ClassesRoot.OpenSubKey("Directory\\shell");
await ParseRegistryAndAddToList(shellList, classRootDirectoryShellKey);
}
else
{
using RegistryKey classRootShellKey = Registry.ClassesRoot.OpenSubKey("*\\shell");
await ParseRegistryAndAddToList(shellList, classRootShellKey);

using RegistryKey classRootFileExtensionShellKey = Registry.ClassesRoot.OpenSubKey($"{fileExtension}\\shell");
await ParseRegistryAndAddToList(shellList, classRootFileExtensionShellKey);

using RegistryKey currentUserShellKey = Registry.CurrentUser.OpenSubKey("Software\\Classes\\*\\shell");
await ParseRegistryAndAddToList(shellList, currentUserShellKey);

using RegistryKey currentUserFileExtensionShellKey = Registry.CurrentUser.OpenSubKey($"Software\\Classes\\{fileExtension}\\shell");
await ParseRegistryAndAddToList(shellList, currentUserFileExtensionShellKey);

}

return shellList;
}
catch
{
return shellList;
}
}
}
}
Loading