From f7be178d184e290e5dd4180423116bcb0c4d840f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 14 Jan 2022 14:13:18 +0100 Subject: [PATCH] Fixes #2605: Add possibility to add submenu items --- ILSpy/AboutPage.cs | 2 +- ILSpy/Commands/CheckForUpdatesCommand.cs | 2 +- ILSpy/Commands/DecompileAllCommand.cs | 4 +- ILSpy/Commands/DisassembleAllCommand.cs | 2 +- ILSpy/Commands/ExitCommand.cs | 2 +- ILSpy/Commands/ExportCommandAttribute.cs | 33 +++++-- ILSpy/Commands/GeneratePdbContextMenuEntry.cs | 2 +- ILSpy/Commands/ManageAssemblyListsCommand.cs | 2 +- ILSpy/Commands/OpenCommand.cs | 2 +- ILSpy/Commands/OpenFromGacCommand.cs | 2 +- ILSpy/Commands/Pdb2XmlCommand.cs | 2 +- ILSpy/Commands/RefreshCommand.cs | 2 +- .../RemoveAssembliesWithLoadErrors.cs | 2 +- ILSpy/Commands/SaveCommand.cs | 2 +- ILSpy/Commands/SortAssemblyListCommand.cs | 4 +- ILSpy/ContextMenuEntry.cs | 98 +++++++++++++------ ILSpy/Docking/CloseAllDocumentsCommand.cs | 4 +- ILSpy/ExtensionMethods.cs | 16 +-- ILSpy/MainWindow.xaml.cs | 88 ++++++++++++----- ILSpy/Options/OptionsDialog.xaml.cs | 2 +- TestPlugin/MainMenuCommand.cs | 2 +- 21 files changed, 181 insertions(+), 94 deletions(-) diff --git a/ILSpy/AboutPage.cs b/ILSpy/AboutPage.cs index bfe32f3cd..1eafd77e4 100644 --- a/ILSpy/AboutPage.cs +++ b/ILSpy/AboutPage.cs @@ -38,7 +38,7 @@ using ICSharpCode.ILSpy.Themes; 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) diff --git a/ILSpy/Commands/CheckForUpdatesCommand.cs b/ILSpy/Commands/CheckForUpdatesCommand.cs index b7fbb7c9b..c398c1c1d 100644 --- a/ILSpy/Commands/CheckForUpdatesCommand.cs +++ b/ILSpy/Commands/CheckForUpdatesCommand.cs @@ -21,7 +21,7 @@ using ICSharpCode.ILSpy.Properties; 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) diff --git a/ILSpy/Commands/DecompileAllCommand.cs b/ILSpy/Commands/DecompileAllCommand.cs index db4b24c87..ca6f926c5 100644 --- a/ILSpy/Commands/DecompileAllCommand.cs +++ b/ILSpy/Commands/DecompileAllCommand.cs @@ -30,7 +30,7 @@ using ICSharpCode.ILSpy.TextView; 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) @@ -79,7 +79,7 @@ namespace ICSharpCode.ILSpy } } - [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) diff --git a/ILSpy/Commands/DisassembleAllCommand.cs b/ILSpy/Commands/DisassembleAllCommand.cs index c34bea449..849dd15d7 100644 --- a/ILSpy/Commands/DisassembleAllCommand.cs +++ b/ILSpy/Commands/DisassembleAllCommand.cs @@ -28,7 +28,7 @@ using ICSharpCode.ILSpy.TextView; 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) diff --git a/ILSpy/Commands/ExitCommand.cs b/ILSpy/Commands/ExitCommand.cs index 34ad7f93f..bbb68122c 100644 --- a/ILSpy/Commands/ExitCommand.cs +++ b/ILSpy/Commands/ExitCommand.cs @@ -19,7 +19,7 @@ using ICSharpCode.ILSpy.Properties; 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) diff --git a/ILSpy/Commands/ExportCommandAttribute.cs b/ILSpy/Commands/ExportCommandAttribute.cs index f8db38bbc..df72bb046 100644 --- a/ILSpy/Commands/ExportCommandAttribute.cs +++ b/ILSpy/Commands/ExportCommandAttribute.cs @@ -52,8 +52,11 @@ namespace ICSharpCode.ILSpy #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; } @@ -65,22 +68,36 @@ namespace ICSharpCode.ILSpy [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class ExportMainMenuCommandAttribute : ExportAttribute, IMainMenuCommandMetadata { - bool isEnabled = true; - public ExportMainMenuCommandAttribute() : base("MainMenuCommand", typeof(ICommand)) { } - + /// + /// 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. + /// + /// NOTE: Defining cycles (for example by accidentally setting equal to ) + /// will lead to a stack-overflow and crash of ILSpy at startup. + /// + public string MenuID { get; set; } public string MenuIcon { get; set; } public string Header { get; set; } - public string Menu { get; set; } + /// + /// 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 , the menu item is displayed in the top-level menu. + /// The built-in menus have the IDs "_File", "_View", "_Window" and "_Help". + /// + /// NOTE: Defining cycles (for example by accidentally setting equal to ) + /// will lead to a stack-overflow and crash of ILSpy at startup. + /// + 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 diff --git a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs index 2577206e6..473360ff5 100644 --- a/ILSpy/Commands/GeneratePdbContextMenuEntry.cs +++ b/ILSpy/Commands/GeneratePdbContextMenuEntry.cs @@ -98,7 +98,7 @@ namespace ICSharpCode.ILSpy } } - [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) diff --git a/ILSpy/Commands/ManageAssemblyListsCommand.cs b/ILSpy/Commands/ManageAssemblyListsCommand.cs index 10ba404d2..ec58ff59d 100644 --- a/ILSpy/Commands/ManageAssemblyListsCommand.cs +++ b/ILSpy/Commands/ManageAssemblyListsCommand.cs @@ -21,7 +21,7 @@ using ICSharpCode.ILSpy.Properties; 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) diff --git a/ILSpy/Commands/OpenCommand.cs b/ILSpy/Commands/OpenCommand.cs index 9e4030cc8..a1663a499 100644 --- a/ILSpy/Commands/OpenCommand.cs +++ b/ILSpy/Commands/OpenCommand.cs @@ -23,7 +23,7 @@ using ICSharpCode.ILSpy.Properties; 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() diff --git a/ILSpy/Commands/OpenFromGacCommand.cs b/ILSpy/Commands/OpenFromGacCommand.cs index e9d72811a..8f72d3b6a 100644 --- a/ILSpy/Commands/OpenFromGacCommand.cs +++ b/ILSpy/Commands/OpenFromGacCommand.cs @@ -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) diff --git a/ILSpy/Commands/Pdb2XmlCommand.cs b/ILSpy/Commands/Pdb2XmlCommand.cs index eada39b20..f64478feb 100644 --- a/ILSpy/Commands/Pdb2XmlCommand.cs +++ b/ILSpy/Commands/Pdb2XmlCommand.cs @@ -33,7 +33,7 @@ using Microsoft.DiaSymReader.Tools; 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) diff --git a/ILSpy/Commands/RefreshCommand.cs b/ILSpy/Commands/RefreshCommand.cs index b81c2260e..3e188a7e6 100644 --- a/ILSpy/Commands/RefreshCommand.cs +++ b/ILSpy/Commands/RefreshCommand.cs @@ -23,7 +23,7 @@ using ICSharpCode.ILSpy.Properties; 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() diff --git a/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs b/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs index c368a8179..a0ca79479 100644 --- a/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs +++ b/ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs @@ -24,7 +24,7 @@ using ICSharpCode.ILSpy.Properties; 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) diff --git a/ILSpy/Commands/SaveCommand.cs b/ILSpy/Commands/SaveCommand.cs index b8ca82bb1..9f07f518f 100644 --- a/ILSpy/Commands/SaveCommand.cs +++ b/ILSpy/Commands/SaveCommand.cs @@ -22,7 +22,7 @@ using ICSharpCode.ILSpy.Properties; 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() diff --git a/ILSpy/Commands/SortAssemblyListCommand.cs b/ILSpy/Commands/SortAssemblyListCommand.cs index a9e09226a..a57999a71 100644 --- a/ILSpy/Commands/SortAssemblyListCommand.cs +++ b/ILSpy/Commands/SortAssemblyListCommand.cs @@ -24,7 +24,7 @@ using ICSharpCode.TreeView; 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 { @@ -40,7 +40,7 @@ namespace ICSharpCode.ILSpy } } - [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 { diff --git a/ILSpy/ContextMenuEntry.cs b/ILSpy/ContextMenuEntry.cs index d7b10617b..f13d6749c 100644 --- a/ILSpy/ContextMenuEntry.cs +++ b/ILSpy/ContextMenuEntry.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using System.Windows; @@ -117,6 +118,8 @@ namespace ICSharpCode.ILSpy public interface IContextMenuEntryMetadata { + string MenuID { get; } + string ParentMenuID { get; } string Icon { get; } string Header { get; } string Category { get; } @@ -135,7 +138,23 @@ namespace ICSharpCode.ILSpy // entries default to end of menu unless given specific order position Order = double.MaxValue; } - + /// + /// 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. + /// + /// NOTE: Defining cycles (for example by accidentally setting equal to ) + /// will lead to a stack-overflow and crash of ILSpy at startup. + /// + public string MenuID { get; set; } + /// + /// 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 , the menu item is displayed in the top-level menu. + /// + /// NOTE: Defining cycles (for example by accidentally setting equal to ) + /// will lead to a stack-overflow and crash of ILSpy at startup. + /// + public string ParentMenuID { get; set; } public string Icon { get; set; } public string Header { get; set; } public string Category { get; set; } @@ -267,41 +286,64 @@ namespace ICSharpCode.ILSpy 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[]>(); + Lazy[] 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>(), menu.Items); + return menu.Items.Count > 0; + + void BuildMenu(Lazy[] 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; } } } diff --git a/ILSpy/Docking/CloseAllDocumentsCommand.cs b/ILSpy/Docking/CloseAllDocumentsCommand.cs index d08af41e8..fc33eb8cd 100644 --- a/ILSpy/Docking/CloseAllDocumentsCommand.cs +++ b/ILSpy/Docking/CloseAllDocumentsCommand.cs @@ -8,7 +8,7 @@ using ICSharpCode.ILSpy.Properties; 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) @@ -17,7 +17,7 @@ namespace ICSharpCode.ILSpy.Docking } } - [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) diff --git a/ILSpy/ExtensionMethods.cs b/ILSpy/ExtensionMethods.cs index 23a376c1a..c34331e1d 100644 --- a/ILSpy/ExtensionMethods.cs +++ b/ILSpy/ExtensionMethods.cs @@ -112,20 +112,12 @@ namespace ICSharpCode.ILSpy } } - /* - public static bool IsCustomAttribute(this TypeDefinition type) + internal static void Deconstruct(this KeyValuePair 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) diff --git a/ILSpy/MainWindow.xaml.cs b/ILSpy/MainWindow.xaml.cs index cd9a387a7..8f89011ff 100644 --- a/ILSpy/MainWindow.xaml.cs +++ b/ILSpy/MainWindow.xaml.cs @@ -241,42 +241,78 @@ namespace ICSharpCode.ILSpy void InitMainMenu() { var mainMenuCommands = App.ExportProvider.GetExports("MainMenuCommand"); - foreach (var topLevelMenu in mainMenuCommands.OrderBy(c => c.Metadata.MenuOrder).GroupBy(c => c.Metadata.Menu)) - { - var topLevelMenuItem = mainMenu.Items.OfType().FirstOrDefault(m => (string)m.Tag == topLevelMenu.Key); - if (topLevelMenuItem == null) - { - topLevelMenuItem = new MenuItem(); - topLevelMenuItem.Header = GetResourceString(topLevelMenu.Key); - topLevelMenuItem.Tag = topLevelMenu.Key; - mainMenu.Items.Add(topLevelMenuItem); - } - foreach (var category in topLevelMenu.GroupBy(c => c.Metadata.MenuCategory)) + // Start by constructing the individual flat menus + var parentMenuItems = new Dictionary(); + var menuGroups = mainMenuCommands.OrderBy(c => c.Metadata.MenuOrder).GroupBy(c => c.Metadata.ParentMenuID); + foreach (var menu in menuGroups) + { + // Get or add the target menu item and add all items grouped by menu category + var parentMenuItem = GetOrAddParentMenuItem(menu.Key, menu.Key); + foreach (var category in menu.GroupBy(c => c.Metadata.MenuCategory)) { - if (topLevelMenuItem.Items.Count > 0) + if (parentMenuItem.Items.Count > 0) { - topLevelMenuItem.Items.Add(new Separator()); + parentMenuItem.Items.Add(new Separator() { Tag = category.Key }); } foreach (var entry in category) { - MenuItem menuItem = new MenuItem(); - menuItem.Command = CommandWrapper.Unwrap(entry.Value); - menuItem.Tag = entry.Metadata.Header; - menuItem.Header = GetResourceString(entry.Metadata.Header); - if (!string.IsNullOrEmpty(entry.Metadata.MenuIcon)) + if (menuGroups.Any(g => g.Key == entry.Metadata.MenuID)) { - menuItem.Icon = new Image { - Width = 16, - Height = 16, - Source = Images.Load(entry.Value, entry.Metadata.MenuIcon) - }; + var menuItem = GetOrAddParentMenuItem(entry.Metadata.MenuID, entry.Metadata.Header); + // replace potential dummy text with real name + menuItem.Header = GetResourceString(entry.Metadata.Header); + parentMenuItem.Items.Add(menuItem); } + else + { + MenuItem menuItem = new MenuItem(); + menuItem.Command = CommandWrapper.Unwrap(entry.Value); + menuItem.Tag = entry.Metadata.MenuID; + menuItem.Header = GetResourceString(entry.Metadata.Header); + if (!string.IsNullOrEmpty(entry.Metadata.MenuIcon)) + { + menuItem.Icon = new Image { + Width = 16, + Height = 16, + Source = Images.Load(entry.Value, entry.Metadata.MenuIcon) + }; + } + + menuItem.IsEnabled = entry.Metadata.IsEnabled; + menuItem.InputGestureText = entry.Metadata.InputGestureText; + parentMenuItem.Items.Add(menuItem); + } + } + } + } - menuItem.IsEnabled = entry.Metadata.IsEnabled; - menuItem.InputGestureText = entry.Metadata.InputGestureText; - topLevelMenuItem.Items.Add(menuItem); + foreach (var (key, item) in parentMenuItems) + { + if (item.Parent == null) + { + mainMenu.Items.Add(item); + } + } + + MenuItem GetOrAddParentMenuItem(string menuID, string resourceKey) + { + if (!parentMenuItems.TryGetValue(menuID, out var parentMenuItem)) + { + var topLevelMenuItem = mainMenu.Items.OfType().FirstOrDefault(m => (string)m.Tag == menuID); + if (topLevelMenuItem == null) + { + parentMenuItem = new MenuItem(); + parentMenuItem.Header = GetResourceString(resourceKey); + parentMenuItem.Tag = menuID; + parentMenuItems.Add(menuID, parentMenuItem); + } + else + { + parentMenuItems.Add(menuID, topLevelMenuItem); + parentMenuItem = topLevelMenuItem; } } + return parentMenuItem; } } diff --git a/ILSpy/Options/OptionsDialog.xaml.cs b/ILSpy/Options/OptionsDialog.xaml.cs index 166f335ec..262853e31 100644 --- a/ILSpy/Options/OptionsDialog.xaml.cs +++ b/ILSpy/Options/OptionsDialog.xaml.cs @@ -121,7 +121,7 @@ namespace ICSharpCode.ILSpy.Options public int Order { get; set; } } - [ExportMainMenuCommand(Menu = nameof(Resources._View), Header = nameof(Resources._Options), MenuCategory = nameof(Resources.Options), MenuOrder = 999)] + [ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources._Options), MenuCategory = nameof(Resources.Options), MenuOrder = 999)] sealed class ShowOptionsCommand : SimpleCommand { public override void Execute(object parameter) diff --git a/TestPlugin/MainMenuCommand.cs b/TestPlugin/MainMenuCommand.cs index 2a83f7c63..501d43b1b 100644 --- a/TestPlugin/MainMenuCommand.cs +++ b/TestPlugin/MainMenuCommand.cs @@ -10,7 +10,7 @@ namespace TestPlugin // Header: text on the menu item // MenuCategory: optional, used for grouping related menu items together. A separator is added between different groups. // MenuOrder: controls the order in which the items appear (items are sorted by this value) - [ExportMainMenuCommand(Menu = "_File", MenuIcon = "Clear.png", Header = "_Clear List", MenuCategory = "Open", MenuOrder = 1.5)] + [ExportMainMenuCommand(ParentMenuID = "_File", MenuIcon = "Clear.png", Header = "_Clear List", MenuCategory = "Open", MenuOrder = 1.5)] // ToolTip: the tool tip // ToolbarIcon: The icon. Must be embedded as "Resource" (WPF-style resource) in the same assembly as the command type. // ToolbarCategory: optional, used for grouping related toolbar items together. A separator is added between different groups.