Browse Source

Merge pull request #2608 from icsharpcode/submenu-support

Submenu support
pull/2626/head
Siegfried Pammer 3 years ago committed by GitHub
parent
commit
2ee248bc2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      ILSpy/AboutPage.cs
  2. 2
      ILSpy/Commands/CheckForUpdatesCommand.cs
  3. 4
      ILSpy/Commands/DecompileAllCommand.cs
  4. 2
      ILSpy/Commands/DisassembleAllCommand.cs
  5. 2
      ILSpy/Commands/ExitCommand.cs
  6. 33
      ILSpy/Commands/ExportCommandAttribute.cs
  7. 2
      ILSpy/Commands/GeneratePdbContextMenuEntry.cs
  8. 2
      ILSpy/Commands/ManageAssemblyListsCommand.cs
  9. 2
      ILSpy/Commands/OpenCommand.cs
  10. 2
      ILSpy/Commands/OpenFromGacCommand.cs
  11. 2
      ILSpy/Commands/Pdb2XmlCommand.cs
  12. 2
      ILSpy/Commands/RefreshCommand.cs
  13. 2
      ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs
  14. 2
      ILSpy/Commands/SaveCommand.cs
  15. 4
      ILSpy/Commands/SortAssemblyListCommand.cs
  16. 98
      ILSpy/ContextMenuEntry.cs
  17. 4
      ILSpy/Docking/CloseAllDocumentsCommand.cs
  18. 16
      ILSpy/ExtensionMethods.cs
  19. 6
      ILSpy/MainWindow.xaml
  20. 103
      ILSpy/MainWindow.xaml.cs
  21. 2
      ILSpy/Options/OptionsDialog.xaml.cs
  22. 2
      TestPlugin/MainMenuCommand.cs

2
ILSpy/AboutPage.cs

@ -38,7 +38,7 @@ using ICSharpCode.ILSpy.Themes; @@ -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)

2
ILSpy/Commands/CheckForUpdatesCommand.cs

@ -21,7 +21,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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)

4
ILSpy/Commands/DecompileAllCommand.cs

@ -30,7 +30,7 @@ using ICSharpCode.ILSpy.TextView; @@ -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 @@ -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)

2
ILSpy/Commands/DisassembleAllCommand.cs

@ -28,7 +28,7 @@ using ICSharpCode.ILSpy.TextView; @@ -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)

2
ILSpy/Commands/ExitCommand.cs

@ -19,7 +19,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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)

33
ILSpy/Commands/ExportCommandAttribute.cs

@ -52,8 +52,11 @@ namespace ICSharpCode.ILSpy @@ -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 @@ -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))
{
}
/// <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

2
ILSpy/Commands/GeneratePdbContextMenuEntry.cs

@ -98,7 +98,7 @@ namespace ICSharpCode.ILSpy @@ -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)

2
ILSpy/Commands/ManageAssemblyListsCommand.cs

@ -21,7 +21,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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)

2
ILSpy/Commands/OpenCommand.cs

@ -23,7 +23,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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()

2
ILSpy/Commands/OpenFromGacCommand.cs

@ -19,7 +19,7 @@ @@ -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)

2
ILSpy/Commands/Pdb2XmlCommand.cs

@ -33,7 +33,7 @@ using Microsoft.DiaSymReader.Tools; @@ -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)

2
ILSpy/Commands/RefreshCommand.cs

@ -23,7 +23,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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()

2
ILSpy/Commands/RemoveAssembliesWithLoadErrors.cs

@ -24,7 +24,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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)

2
ILSpy/Commands/SaveCommand.cs

@ -22,7 +22,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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()

4
ILSpy/Commands/SortAssemblyListCommand.cs

@ -24,7 +24,7 @@ using ICSharpCode.TreeView; @@ -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<LoadedAssembly>
{
@ -40,7 +40,7 @@ namespace ICSharpCode.ILSpy @@ -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
{

98
ILSpy/ContextMenuEntry.cs

@ -17,6 +17,7 @@ @@ -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 @@ -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 @@ -135,7 +138,23 @@ namespace ICSharpCode.ILSpy
// 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; }
@ -267,41 +286,64 @@ namespace ICSharpCode.ILSpy @@ -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<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
ILSpy/Docking/CloseAllDocumentsCommand.cs

@ -8,7 +8,7 @@ using ICSharpCode.ILSpy.Properties; @@ -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 @@ -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)

16
ILSpy/ExtensionMethods.cs

@ -112,20 +112,12 @@ namespace ICSharpCode.ILSpy @@ -112,20 +112,12 @@ namespace ICSharpCode.ILSpy
}
}
/*
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)

6
ILSpy/MainWindow.xaml

@ -132,9 +132,9 @@ @@ -132,9 +132,9 @@
<DockPanel>
<!-- Main menu -->
<Menu DockPanel.Dock="Top" Name="mainMenu" Height="23" KeyboardNavigation.TabNavigation="None">
<MenuItem Header="{x:Static properties:Resources._File}" />
<MenuItem Header="{x:Static properties:Resources._File}" Tag="_File" />
<!-- contents of file menu are added using MEF -->
<MenuItem Header="{x:Static properties:Resources._View}">
<MenuItem Header="{x:Static properties:Resources._View}" Tag="_View">
<MenuItem Header="{x:Static properties:Resources.Show_publiconlyTypesMembers}" IsCheckable="True" IsChecked="{Binding Workspace.ActiveTabPage.FilterSettings.ApiVisPublicOnly}" />
<MenuItem Header="{x:Static properties:Resources.Show_internalTypesMembers}" IsCheckable="True" IsChecked="{Binding Workspace.ActiveTabPage.FilterSettings.ApiVisPublicAndInternal}" />
<MenuItem Header="{x:Static properties:Resources.Show_allTypesAndMembers}" IsCheckable="True" IsChecked="{Binding Workspace.ActiveTabPage.FilterSettings.ApiVisAll}" />
@ -145,7 +145,7 @@ @@ -145,7 +145,7 @@
<MenuItem Header="中文" IsCheckable="True" IsChecked="{Binding SessionSettings.CurrentCulture, Converter={StaticResource cultureSelectionConverter}, ConverterParameter=zh-Hans}" />
</MenuItem>
</MenuItem>
<MenuItem Header="{x:Static properties:Resources._Window}" />
<MenuItem Header="{x:Static properties:Resources._Window}" Tag="_Window" />
</Menu>
<!-- ToolBar -->
<ToolBar

103
ILSpy/MainWindow.xaml.cs

@ -146,7 +146,7 @@ namespace ICSharpCode.ILSpy @@ -146,7 +146,7 @@ namespace ICSharpCode.ILSpy
filterSettings = dock.ActiveTabPage.FilterSettings;
filterSettings.PropertyChanged += filterSettings_PropertyChanged;
var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => GetResourceString(m.Header as string) == Properties.Resources._Window);
var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == Properties.Resources._Window);
foreach (MenuItem menuItem in windowMenuItem.Items.OfType<MenuItem>())
{
if (menuItem.IsCheckable && menuItem.Tag is TabPageModel)
@ -241,48 +241,93 @@ namespace ICSharpCode.ILSpy @@ -241,48 +241,93 @@ namespace ICSharpCode.ILSpy
void InitMainMenu()
{
var mainMenuCommands = App.ExportProvider.GetExports<ICommand, IMainMenuCommandMetadata>("MainMenuCommand");
foreach (var topLevelMenu in mainMenuCommands.OrderBy(c => c.Metadata.MenuOrder).GroupBy(c => GetResourceString(c.Metadata.Menu)))
{
var topLevelMenuItem = mainMenu.Items.OfType<MenuItem>().FirstOrDefault(m => GetResourceString(m.Header as string) == topLevelMenu.Key);
foreach (var category in topLevelMenu.GroupBy(c => GetResourceString(c.Metadata.MenuCategory)))
// Start by constructing the individual flat menus
var parentMenuItems = new Dictionary<string, MenuItem>();
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 == null)
{
topLevelMenuItem = new MenuItem();
topLevelMenuItem.Header = GetResourceString(topLevelMenu.Key);
mainMenu.Items.Add(topLevelMenuItem);
}
else 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);
if (!string.IsNullOrEmpty(GetResourceString(entry.Metadata.Header)))
if (menuGroups.Any(g => g.Key == entry.Metadata.MenuID))
{
var menuItem = GetOrAddParentMenuItem(entry.Metadata.MenuID, entry.Metadata.Header);
// replace potential dummy text with real name
menuItem.Header = GetResourceString(entry.Metadata.Header);
if (!string.IsNullOrEmpty(entry.Metadata.MenuIcon))
parentMenuItem.Items.Add(menuItem);
}
else
{
menuItem.Icon = new Image {
Width = 16,
Height = 16,
Source = Images.Load(entry.Value, entry.Metadata.MenuIcon)
};
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);
}
}
}
}
foreach (var (key, item) in parentMenuItems)
{
if (item.Parent == null)
{
mainMenu.Items.Add(item);
}
}
menuItem.IsEnabled = entry.Metadata.IsEnabled;
menuItem.InputGestureText = entry.Metadata.InputGestureText;
topLevelMenuItem.Items.Add(menuItem);
MenuItem GetOrAddParentMenuItem(string menuID, string resourceKey)
{
if (!parentMenuItems.TryGetValue(menuID, out var parentMenuItem))
{
var topLevelMenuItem = mainMenu.Items.OfType<MenuItem>().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;
}
}
internal static string GetResourceString(string key)
{
var str = !string.IsNullOrEmpty(key) ? Properties.Resources.ResourceManager.GetString(key) : null;
return string.IsNullOrEmpty(key) || string.IsNullOrEmpty(str) ? key : str;
if (string.IsNullOrEmpty(key))
{
return null;
}
string value = Properties.Resources.ResourceManager.GetString(key);
if (!string.IsNullOrEmpty(value))
{
return value;
}
return key;
}
#endregion
@ -310,7 +355,7 @@ namespace ICSharpCode.ILSpy @@ -310,7 +355,7 @@ namespace ICSharpCode.ILSpy
private void InitWindowMenu()
{
var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => GetResourceString(m.Header as string) == Properties.Resources._Window);
var windowMenuItem = mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == Properties.Resources._Window);
Separator separatorBeforeTools, separatorBeforeDocuments;
windowMenuItem.Items.Add(separatorBeforeTools = new Separator());
windowMenuItem.Items.Add(separatorBeforeDocuments = new Separator());
@ -472,7 +517,7 @@ namespace ICSharpCode.ILSpy @@ -472,7 +517,7 @@ namespace ICSharpCode.ILSpy
static void TabPageChanged(object sender, PropertyChangedEventArgs e)
{
var windowMenuItem = Instance.mainMenu.Items.OfType<MenuItem>().First(m => GetResourceString(m.Header as string) == Properties.Resources._Window);
var windowMenuItem = Instance.mainMenu.Items.OfType<MenuItem>().First(m => (string)m.Tag == nameof(Properties.Resources._Window));
foreach (MenuItem menuItem in windowMenuItem.Items.OfType<MenuItem>())
{
if (menuItem.IsCheckable && menuItem.Tag == sender)

2
ILSpy/Options/OptionsDialog.xaml.cs

@ -121,7 +121,7 @@ namespace ICSharpCode.ILSpy.Options @@ -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)

2
TestPlugin/MainMenuCommand.cs

@ -10,7 +10,7 @@ namespace TestPlugin @@ -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.

Loading…
Cancel
Save