Skip to content

Commit

Permalink
Fixes icsharpcode#2605: Add possibility to add submenu items
Browse files Browse the repository at this point in the history
  • Loading branch information
siegfriedpammer committed Jan 24, 2022
1 parent 94ffb32 commit f7be178
Show file tree
Hide file tree
Showing 21 changed files with 181 additions and 94 deletions.
2 changes: 1 addition & 1 deletion ILSpy/AboutPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._Help), Header = nameof(Resources._About), MenuOrder = 99999)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._Help), Header = nameof(Resources._About), MenuOrder = 99999)]
sealed class AboutPage : SimpleCommand
{
public override void Execute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/CheckForUpdatesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._Help), Header = nameof(Resources._CheckUpdates), MenuOrder = 5000)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._Help), Header = nameof(Resources._CheckUpdates), MenuOrder = 5000)]
sealed class CheckForUpdatesCommand : SimpleCommand
{
public override bool CanExecute(object parameter)
Expand Down
4 changes: 2 additions & 2 deletions ILSpy/Commands/DecompileAllCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)]
sealed class DecompileAllCommand : SimpleCommand
{
public override bool CanExecute(object parameter)
Expand Down Expand Up @@ -79,7 +79,7 @@ public override void Execute(object parameter)
}
}

[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile100x), MenuCategory = nameof(Resources.Open), MenuOrder = 2.6)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDecompile100x), MenuCategory = nameof(Resources.Open), MenuOrder = 2.6)]
sealed class Decompile100TimesCommand : SimpleCommand
{
public override void Execute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/DisassembleAllCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.DEBUGDisassemble), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDisassemble), MenuCategory = nameof(Resources.Open), MenuOrder = 2.5)]
sealed class DisassembleAllCommand : SimpleCommand
{
public override bool CanExecute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/ExitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.E_xit), MenuOrder = 99999, MenuCategory = nameof(Resources.Exit))]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.E_xit), MenuOrder = 99999, MenuCategory = nameof(Resources.Exit))]
sealed class ExitCommand : SimpleCommand
{
public override void Execute(object parameter)
Expand Down
33 changes: 25 additions & 8 deletions ILSpy/Commands/ExportCommandAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,11 @@ public ExportToolbarCommandAttribute()
#region Main Menu
public interface IMainMenuCommandMetadata
{
string MenuID { get; }
string MenuIcon { get; }
string Header { get; }
string ParentMenuID { get; }
[Obsolete("Please use ParentMenuID instead. We decided to rename the property for clarity. It will be removed in ILSpy 8.0.")]
string Menu { get; }
string MenuCategory { get; }
string InputGestureText { get; }
Expand All @@ -65,22 +68,36 @@ public interface IMainMenuCommandMetadata
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ExportMainMenuCommandAttribute : ExportAttribute, IMainMenuCommandMetadata
{
bool isEnabled = true;

public ExportMainMenuCommandAttribute()
: base("MainMenuCommand", typeof(ICommand))
{
}

/// <summary>
/// Gets/Sets the ID of this menu item. Menu entries are not required to have an ID,
/// however, setting it allows to declare nested menu structures.
/// The built-in menus have the IDs "_File", "_View", "_Window" and "_Help".
/// Plugin authors are advised to use GUIDs as identifiers to prevent conflicts.
/// <para/>
/// NOTE: Defining cycles (for example by accidentally setting <see cref="MenuID"/> equal to <see cref="ParentMenuID"/>)
/// will lead to a stack-overflow and crash of ILSpy at startup.
/// </summary>
public string MenuID { get; set; }
public string MenuIcon { get; set; }
public string Header { get; set; }
public string Menu { get; set; }
/// <summary>
/// Gets/Sets the parent of this menu item. All menu items sharing the same parent will be displayed as sub-menu items.
/// If this property is set to <see langword="null"/>, the menu item is displayed in the top-level menu.
/// The built-in menus have the IDs "_File", "_View", "_Window" and "_Help".
/// <para/>
/// NOTE: Defining cycles (for example by accidentally setting <see cref="MenuID"/> equal to <see cref="ParentMenuID"/>)
/// will lead to a stack-overflow and crash of ILSpy at startup.
/// </summary>
public string ParentMenuID { get; set; }
[Obsolete("Please use ParentMenuID instead. We decided to rename the property for clarity. It will be removed in ILSpy 8.0.")]
public string Menu { get => ParentMenuID; set => ParentMenuID = value; }
public string MenuCategory { get; set; }
public string InputGestureText { get; set; }
public bool IsEnabled {
get { return isEnabled; }
set { isEnabled = value; }
}
public bool IsEnabled { get; set; } = true;
public double MenuOrder { get; set; }
}
#endregion
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/GeneratePdbContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ internal static void GeneratePdbForAssembly(LoadedAssembly assembly)
}
}

[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.GeneratePortable), MenuCategory = nameof(Resources.Save))]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.GeneratePortable), MenuCategory = nameof(Resources.Save))]
class GeneratePdbMainMenuEntry : SimpleCommand
{
public override bool CanExecute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/ManageAssemblyListsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.ManageAssembly_Lists), MenuIcon = "Images/AssemblyList", MenuCategory = nameof(Resources.Open), MenuOrder = 1.7)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.ManageAssembly_Lists), MenuIcon = "Images/AssemblyList", MenuCategory = nameof(Resources.Open), MenuOrder = 1.7)]
sealed class ManageAssemblyListsCommand : SimpleCommand
{
public override void Execute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/OpenCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
namespace ICSharpCode.ILSpy
{
[ExportToolbarCommand(ToolTip = nameof(Resources.Open), ToolbarIcon = "Images/Open", ToolbarCategory = nameof(Resources.Open), ToolbarOrder = 0)]
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources._Open), MenuIcon = "Images/Open", MenuCategory = nameof(Resources.Open), MenuOrder = 0)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._Open), MenuIcon = "Images/Open", MenuCategory = nameof(Resources.Open), MenuOrder = 0)]
sealed class OpenCommand : CommandWrapper
{
public OpenCommand()
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/OpenFromGacCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
using ICSharpCode.ILSpy.Properties;
namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.OpenFrom_GAC), MenuIcon = "Images/AssemblyListGAC", MenuCategory = nameof(Resources.Open), MenuOrder = 1)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.OpenFrom_GAC), MenuIcon = "Images/AssemblyListGAC", MenuCategory = nameof(Resources.Open), MenuOrder = 1)]
sealed class OpenFromGacCommand : SimpleCommand
{
public override void Execute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/Pdb2XmlCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources.DEBUGDumpPDBAsXML), MenuCategory = nameof(Resources.Open), MenuOrder = 2.6)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources.DEBUGDumpPDBAsXML), MenuCategory = nameof(Resources.Open), MenuOrder = 2.6)]
sealed class Pdb2XmlCommand : SimpleCommand
{
public override bool CanExecute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/RefreshCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
namespace ICSharpCode.ILSpy
{
[ExportToolbarCommand(ToolTip = nameof(Resources.RefreshCommand_ReloadAssemblies), ToolbarIcon = "Images/Refresh", ToolbarCategory = nameof(Resources.Open), ToolbarOrder = 2)]
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources._Reload), MenuIcon = "Images/Refresh", MenuCategory = nameof(Resources.Open), MenuOrder = 2)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._Reload), MenuIcon = "Images/Refresh", MenuCategory = nameof(Resources.Open), MenuOrder = 2)]
sealed class RefreshCommand : CommandWrapper
{
public RefreshCommand()
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources._RemoveAssembliesWithLoadErrors), MenuCategory = nameof(Resources.Remove), MenuOrder = 2.6)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._RemoveAssembliesWithLoadErrors), MenuCategory = nameof(Resources.Remove), MenuOrder = 2.6)]
class RemoveAssembliesWithLoadErrors : SimpleCommand
{
public override bool CanExecute(object parameter)
Expand Down
2 changes: 1 addition & 1 deletion ILSpy/Commands/SaveCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._File), Header = nameof(Resources._SaveCode), MenuIcon = "Images/Save", MenuCategory = nameof(Resources.Save), MenuOrder = 0)]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._SaveCode), MenuIcon = "Images/Save", MenuCategory = nameof(Resources.Save), MenuOrder = 0)]
sealed class SaveCommand : CommandWrapper
{
public SaveCommand()
Expand Down
4 changes: 2 additions & 2 deletions ILSpy/Commands/SortAssemblyListCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

namespace ICSharpCode.ILSpy
{
[ExportMainMenuCommand(Menu = nameof(Resources._View), Header = nameof(Resources.SortAssembly_listName), MenuIcon = "Images/Sort", MenuCategory = nameof(Resources.View))]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources.SortAssembly_listName), MenuIcon = "Images/Sort", MenuCategory = nameof(Resources.View))]
[ExportToolbarCommand(ToolTip = nameof(Resources.SortAssemblyListName), ToolbarIcon = "Images/Sort", ToolbarCategory = nameof(Resources.View))]
sealed class SortAssemblyListCommand : SimpleCommand, IComparer<LoadedAssembly>
{
Expand All @@ -40,7 +40,7 @@ int IComparer<LoadedAssembly>.Compare(LoadedAssembly x, LoadedAssembly y)
}
}

[ExportMainMenuCommand(Menu = nameof(Resources._View), Header = nameof(Resources._CollapseTreeNodes), MenuIcon = "Images/CollapseAll", MenuCategory = nameof(Resources.View))]
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources._CollapseTreeNodes), MenuIcon = "Images/CollapseAll", MenuCategory = nameof(Resources.View))]
[ExportToolbarCommand(ToolTip = nameof(Resources.CollapseTreeNodes), ToolbarIcon = "Images/CollapseAll", ToolbarCategory = nameof(Resources.View))]
sealed class CollapseAllCommand : SimpleCommand
{
Expand Down
98 changes: 70 additions & 28 deletions ILSpy/ContextMenuEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Windows;
Expand Down Expand Up @@ -117,6 +118,8 @@ public static TextViewContext Create(SharpTreeView treeView = null, DecompilerTe

public interface IContextMenuEntryMetadata
{
string MenuID { get; }
string ParentMenuID { get; }
string Icon { get; }
string Header { get; }
string Category { get; }
Expand All @@ -135,7 +138,23 @@ public ExportContextMenuEntryAttribute()
// entries default to end of menu unless given specific order position
Order = double.MaxValue;
}

/// <summary>
/// Gets/Sets the ID of this menu item. Menu entries are not required to have an ID,
/// however, setting it allows to declare nested menu structures.
/// Plugin authors are advised to use GUIDs as identifiers to prevent conflicts.
/// <para/>
/// NOTE: Defining cycles (for example by accidentally setting <see cref="MenuID"/> equal to <see cref="ParentMenuID"/>)
/// will lead to a stack-overflow and crash of ILSpy at startup.
/// </summary>
public string MenuID { get; set; }
/// <summary>
/// Gets/Sets the parent of this menu item. All menu items sharing the same parent will be displayed as sub-menu items.
/// If this property is set to <see langword="null"/>, the menu item is displayed in the top-level menu.
/// <para/>
/// NOTE: Defining cycles (for example by accidentally setting <see cref="MenuID"/> equal to <see cref="ParentMenuID"/>)
/// will lead to a stack-overflow and crash of ILSpy at startup.
/// </summary>
public string ParentMenuID { get; set; }
public string Icon { get; set; }
public string Header { get; set; }
public string Category { get; set; }
Expand Down Expand Up @@ -267,41 +286,64 @@ void dataGrid_ContextMenuOpening(object sender, ContextMenuEventArgs e)
bool ShowContextMenu(TextViewContext context, out ContextMenu menu)
{
menu = new ContextMenu();
foreach (var category in entries.OrderBy(c => c.Metadata.Order).GroupBy(c => c.Metadata.Category))
var menuGroups = new Dictionary<string, Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[]>();
Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[] topLevelGroup = null;
foreach (var group in entries.OrderBy(c => c.Metadata.Order).GroupBy(c => c.Metadata.ParentMenuID))
{
bool needSeparatorForCategory = menu.Items.Count > 0;
foreach (var entryPair in category)
if (group.Key == null)
{
IContextMenuEntry entry = entryPair.Value;
if (entry.IsVisible(context))
topLevelGroup = group.ToArray();
}
else
{
menuGroups.Add(group.Key, group.ToArray());
}
}
BuildMenu(topLevelGroup ?? Array.Empty<Lazy<IContextMenuEntry, IContextMenuEntryMetadata>>(), menu.Items);
return menu.Items.Count > 0;

void BuildMenu(Lazy<IContextMenuEntry, IContextMenuEntryMetadata>[] menuGroup, ItemCollection parent)
{
foreach (var category in menuGroup.GroupBy(c => c.Metadata.Category))
{
bool needSeparatorForCategory = parent.Count > 0;
foreach (var entryPair in category)
{
if (needSeparatorForCategory)
{
menu.Items.Add(new Separator());
needSeparatorForCategory = false;
}
MenuItem menuItem = new MenuItem();
menuItem.Header = MainWindow.GetResourceString(entryPair.Metadata.Header);
menuItem.InputGestureText = entryPair.Metadata.InputGestureText;
if (!string.IsNullOrEmpty(entryPair.Metadata.Icon))
IContextMenuEntry entry = entryPair.Value;
if (entry.IsVisible(context))
{
menuItem.Icon = new Image {
Width = 16,
Height = 16,
Source = Images.Load(entryPair.Value, entryPair.Metadata.Icon)
};
}
if (entryPair.Value.IsEnabled(context))
{
menuItem.Click += delegate { entry.Execute(context); };
if (needSeparatorForCategory)
{
parent.Add(new Separator());
needSeparatorForCategory = false;
}
MenuItem menuItem = new MenuItem();
menuItem.Header = MainWindow.GetResourceString(entryPair.Metadata.Header);
menuItem.InputGestureText = entryPair.Metadata.InputGestureText;
if (!string.IsNullOrEmpty(entryPair.Metadata.Icon))
{
menuItem.Icon = new Image {
Width = 16,
Height = 16,
Source = Images.Load(entryPair.Value, entryPair.Metadata.Icon)
};
}
if (entryPair.Value.IsEnabled(context))
{
menuItem.Click += delegate { entry.Execute(context); };
}
else
menuItem.IsEnabled = false;
parent.Add(menuItem);

if (entryPair.Metadata.MenuID != null && menuGroups.TryGetValue(entryPair.Metadata.MenuID, out var group))
{
BuildMenu(group, menuItem.Items);
}
}
else
menuItem.IsEnabled = false;
menu.Items.Add(menuItem);
}
}
}
return menu.Items.Count > 0;
}
}
}
4 changes: 2 additions & 2 deletions ILSpy/Docking/CloseAllDocumentsCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace ICSharpCode.ILSpy.Docking
{
[ExportMainMenuCommand(Header = nameof(Resources.Window_CloseAllDocuments), Menu = nameof(Resources._Window))]
[ExportMainMenuCommand(Header = nameof(Resources.Window_CloseAllDocuments), ParentMenuID = nameof(Resources._Window))]
class CloseAllDocumentsCommand : SimpleCommand
{
public override void Execute(object parameter)
Expand All @@ -17,7 +17,7 @@ public override void Execute(object parameter)
}
}

[ExportMainMenuCommand(Header = nameof(Resources.Window_ResetLayout), Menu = nameof(Resources._Window))]
[ExportMainMenuCommand(Header = nameof(Resources.Window_ResetLayout), ParentMenuID = nameof(Resources._Window))]
class ResetLayoutCommand : SimpleCommand
{
public override void Execute(object parameter)
Expand Down
16 changes: 4 additions & 12 deletions ILSpy/ExtensionMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,12 @@ public static void InsertSorted<T>(this IList<T> list, T item, IComparer<T> comp
}
}

/*
public static bool IsCustomAttribute(this TypeDefinition type)
internal static void Deconstruct<TKey, TValue>(this KeyValuePair<TKey, TValue> pair, out TKey key, out TValue value)
{
while (type.FullName != "System.Object") {
var resolvedBaseType = type.BaseType.Resolve();
if (resolvedBaseType == null)
return false;
if (resolvedBaseType.FullName == "System.Attribute")
return true;
type = resolvedBaseType;
}
return false;
key = pair.Key;
value = pair.Value;
}
*/

public static string ToSuffixString(this System.Reflection.Metadata.EntityHandle handle)
{
if (!DisplaySettingsPanel.CurrentDisplaySettings.ShowMetadataTokens)
Expand Down
Loading

0 comments on commit f7be178

Please sign in to comment.