From a8def5bf85d2ba1a55b588a6ef194990fffce964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Ku=C4=8Dera?= Date: Sat, 11 Oct 2025 09:50:37 +0100 Subject: [PATCH] Navigate visible history --- ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs | 5 ++ ILSpy/AssemblyTree/AssemblyTreeModel.cs | 23 +++-- ILSpy/Commands/BrowseBackCommand.cs | 13 ++- ILSpy/Commands/BrowseForwardCommand.cs | 11 ++- ILSpy/Commands/SimpleCommand.cs | 7 ++ ILSpy/Controls/MainToolBar.xaml.cs | 89 +++++++++++++++++++- ILSpy/Metadata/MetadataTreeNode.cs | 2 + ILSpy/NavigationHistory.cs | 3 + ILSpy/NavigationState.cs | 18 ++++ ILSpy/Themes/ThemeManager.cs | 6 ++ ILSpy/TreeNodes/BaseTypesTreeNode.cs | 2 + ILSpy/TreeNodes/DerivedTypesTreeNode.cs | 2 + ILSpy/TreeNodes/EventTreeNode.cs | 6 +- ILSpy/TreeNodes/FieldTreeNode.cs | 6 +- ILSpy/TreeNodes/MethodTreeNode.cs | 6 +- ILSpy/TreeNodes/PropertyTreeNode.cs | 6 +- ILSpy/TreeNodes/ReferenceFolderTreeNode.cs | 2 + ILSpy/TreeNodes/ResourceListTreeNode.cs | 2 + ILSpy/TreeNodes/TypeTreeNode.cs | 3 + 19 files changed, 193 insertions(+), 19 deletions(-) diff --git a/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs b/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs index 8a8619080..7a735070c 100644 --- a/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs +++ b/ICSharpCode.ILSpyX/TreeView/SharpTreeNode.cs @@ -126,6 +126,11 @@ namespace ICSharpCode.ILSpyX.TreeView get { return null; } } + public virtual object? NavigationText + { + get { return Text; } + } + public virtual object? Icon { get { return null; } } diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index c201179c1..8a0a390ed 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -853,15 +853,24 @@ namespace ICSharpCode.ILSpy.AssemblyTree #endregion - public void NavigateHistory(bool forward) + public void NavigateHistory(bool forward, NavigationState? toState = null) { try { - TabPageModel tabPage = DockWorkspace.ActiveTabPage; - var state = tabPage.GetState(); - if (state != null) - history.UpdateCurrent(new NavigationState(tabPage, state)); - var newState = forward ? history.GoForward() : history.GoBack(); + TabPageModel tabPage = DockWorkspace.ActiveTabPage; + var currentState = tabPage.GetState(); + if (currentState != null) + history.UpdateCurrent(new NavigationState(tabPage, currentState)); + + NavigationState newState; + do + { + newState = forward ? history.GoForward() : history.GoBack(); + } while (newState != null && toState != null && toState != newState); + + if (newState == null) + return; + navigatingToState = newState; TabPageModel activeTabPage = newState.TabPage; @@ -884,6 +893,8 @@ namespace ICSharpCode.ILSpy.AssemblyTree } } + public NavigationState[] GetNavigateHistory(bool forward) => forward ? history.ForwardList : history.BackList; + public bool CanNavigateBack => history.CanNavigateBack; public bool CanNavigateForward => history.CanNavigateForward; diff --git a/ILSpy/Commands/BrowseBackCommand.cs b/ILSpy/Commands/BrowseBackCommand.cs index d52f4d734..2c2e78798 100644 --- a/ILSpy/Commands/BrowseBackCommand.cs +++ b/ILSpy/Commands/BrowseBackCommand.cs @@ -16,7 +16,9 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Collections; using System.Composition; +using System.Linq; using System.Windows.Input; using ICSharpCode.ILSpy.AssemblyTree; @@ -26,7 +28,7 @@ namespace ICSharpCode.ILSpy { [ExportToolbarCommand(ToolTip = nameof(Resources.Back), ToolbarIcon = "Images/Back", ToolbarCategory = nameof(Resources.Navigation), ToolbarOrder = 0)] [Shared] - sealed class BrowseBackCommand : CommandWrapper + sealed class BrowseBackCommand : CommandWrapper, IProvideParameterList { readonly AssemblyTreeModel assemblyTreeModel; @@ -49,8 +51,15 @@ namespace ICSharpCode.ILSpy if (assemblyTreeModel.CanNavigateBack) { e.Handled = true; - assemblyTreeModel.NavigateHistory(false); + assemblyTreeModel.NavigateHistory(false, e.Parameter as NavigationState); } } + + public IEnumerable ParameterList => assemblyTreeModel.GetNavigateHistory(false).Reverse(); + + public object GetParamaterText(object parameter) + { + return (parameter as NavigationState)?.NavigationText; + } } } diff --git a/ILSpy/Commands/BrowseForwardCommand.cs b/ILSpy/Commands/BrowseForwardCommand.cs index 1eb264856..74b42a1cd 100644 --- a/ILSpy/Commands/BrowseForwardCommand.cs +++ b/ILSpy/Commands/BrowseForwardCommand.cs @@ -16,6 +16,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Collections; using System.Composition; using System.Windows.Input; @@ -26,7 +27,7 @@ namespace ICSharpCode.ILSpy { [ExportToolbarCommand(ToolTip = nameof(Resources.Forward), ToolbarIcon = "Images/Forward", ToolbarCategory = nameof(Resources.Navigation), ToolbarOrder = 1)] [Shared] - sealed class BrowseForwardCommand : CommandWrapper + sealed class BrowseForwardCommand : CommandWrapper, IProvideParameterList { private readonly AssemblyTreeModel assemblyTreeModel; @@ -49,9 +50,15 @@ namespace ICSharpCode.ILSpy if (assemblyTreeModel.CanNavigateForward) { e.Handled = true; - assemblyTreeModel.NavigateHistory(true); + assemblyTreeModel.NavigateHistory(true, e.Parameter as NavigationState); } } + public IEnumerable ParameterList => assemblyTreeModel.GetNavigateHistory(true); + + public object GetParamaterText(object parameter) + { + return (parameter as NavigationState)?.NavigationText; + } } } diff --git a/ILSpy/Commands/SimpleCommand.cs b/ILSpy/Commands/SimpleCommand.cs index 19c1c7c9e..da37bc81b 100644 --- a/ILSpy/Commands/SimpleCommand.cs +++ b/ILSpy/Commands/SimpleCommand.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.Collections; using System.Windows.Data; using System.Windows.Input; @@ -41,4 +42,10 @@ namespace ICSharpCode.ILSpy { Binding ParameterBinding { get; } } + + public interface IProvideParameterList + { + IEnumerable ParameterList { get; } + object GetParamaterText(object parameter); + } } diff --git a/ILSpy/Controls/MainToolBar.xaml.cs b/ILSpy/Controls/MainToolBar.xaml.cs index 246d68d6c..52f66a0df 100644 --- a/ILSpy/Controls/MainToolBar.xaml.cs +++ b/ILSpy/Controls/MainToolBar.xaml.cs @@ -23,9 +23,11 @@ using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Input; +using System.Windows.Media; using System.Windows.Threading; using ICSharpCode.ILSpy.Themes; +using ICSharpCode.ILSpyX.TreeView; using TomsToolbox.Composition; @@ -85,7 +87,8 @@ namespace ICSharpCode.ILSpy.Controls } } - static Button CreateToolbarItem(IExport commandExport) + + static UIElement CreateToolbarItem(IExport commandExport) { var command = commandExport.Value; @@ -108,9 +111,93 @@ namespace ICSharpCode.ILSpy.Controls parameterBinding.ParameterBinding); } + if (command is IProvideParameterList parameterList) + { + toolbarItem.Margin = new Thickness(2, 0, 0, 0); + + var dropDownPanel = new StackPanel { Orientation = Orientation.Horizontal }; + + var contextMenu = new ContextMenu { + PlacementTarget = dropDownPanel, + Tag = command + }; + + ContextMenuService.SetPlacement(toolbarItem, PlacementMode.Bottom); + toolbarItem.ContextMenu = contextMenu; + toolbarItem.ContextMenuOpening += (_, _) => + PrepareParameterList(contextMenu); + + var dropDownToggle = new ToggleButton { + Style = ThemeManager.Current.CreateToolBarToggleButtonStyle(), + Content = "▾", + Padding = new Thickness(0), + MinWidth = 0, + Margin = new Thickness(0, 0, 2, 0) + }; + + dropDownToggle.Checked += (_, _) => { + PrepareParameterList(contextMenu); + contextMenu.Placement = PlacementMode.Bottom; + contextMenu.SetCurrentValue(ContextMenu.IsOpenProperty, true); + }; + + BindingOperations.SetBinding(dropDownToggle, ToggleButton.IsCheckedProperty, + new Binding(nameof(contextMenu.IsOpen)) { Source = contextMenu }); + + BindingOperations.SetBinding(dropDownToggle, IsEnabledProperty, + new Binding(nameof(IsEnabled)) { Source = toolbarItem }); + + dropDownPanel.Children.Add(toolbarItem); + dropDownPanel.Children.Add(dropDownToggle); + return dropDownPanel; + } + return toolbarItem; } + static void PrepareParameterList(ContextMenu menu) + { + const int maximumParameterListCount = 20; + + var command = (ICommand)menu.Tag; + var parameterList = (IProvideParameterList)command; + + menu.Items.Clear(); + foreach (var parameter in parameterList.ParameterList) + { + MenuItem parameterItem = new MenuItem(); + parameterItem.Command = CommandWrapper.Unwrap(command); + parameterItem.CommandParameter = parameter; + parameterItem.CommandTarget = menu.PlacementTarget; + parameterItem.InputGestureText = " "; + + var headerPresenter = new ContentPresenter { RecognizesAccessKey = false }; + parameterItem.Header = headerPresenter; + + var header = parameterList.GetParamaterText(parameter); + switch (header) + { + case SharpTreeNode node: + headerPresenter.Content = node.NavigationText; + if (node.Icon is ImageSource icon) + parameterItem.Icon = new Image { + Width = 16, + Height = 16, + Source = icon + }; + break; + + default: + headerPresenter.Content = header; + break; + } + + menu.Items.Add(parameterItem); + if (menu.Items.Count >= maximumParameterListCount) + break; + } + } + void MainWindow_KeyDown(object sender, KeyEventArgs e) { if (e.Handled || e.KeyboardDevice.Modifiers != ModifierKeys.Alt || e.Key != Key.System) diff --git a/ILSpy/Metadata/MetadataTreeNode.cs b/ILSpy/Metadata/MetadataTreeNode.cs index e4fe18b8c..ac386c943 100644 --- a/ILSpy/Metadata/MetadataTreeNode.cs +++ b/ILSpy/Metadata/MetadataTreeNode.cs @@ -45,6 +45,8 @@ namespace ICSharpCode.ILSpy.Metadata public override object Text => title; + public override object NavigationText => metadataFile.Name + " " + Text; + public override object Icon => Images.Metadata; public override bool View(TabPageModel tabPage) diff --git a/ILSpy/NavigationHistory.cs b/ILSpy/NavigationHistory.cs index 30f428a84..6e9f6ed70 100644 --- a/ILSpy/NavigationHistory.cs +++ b/ILSpy/NavigationHistory.cs @@ -34,6 +34,9 @@ namespace ICSharpCode.ILSpy List back = new List(); List forward = new List(); + public T[] BackList => back.ToArray(); + public T[] ForwardList => forward.ToArray(); + public bool CanNavigateBack { get { return back.Count > 0; } } diff --git a/ILSpy/NavigationState.cs b/ILSpy/NavigationState.cs index 287b849cc..dbd5545e2 100644 --- a/ILSpy/NavigationState.cs +++ b/ILSpy/NavigationState.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; @@ -62,5 +63,22 @@ namespace ICSharpCode.ILSpy return this.ViewState.Equals(other.ViewState); } + + public object NavigationText + { + get + { + if (this.treeNodes.Count == 1) + return this.treeNodes.First(); + + if (this.treeNodes.Count > 0) + return string.Join(", ", treeNodes.Select(tn => tn.NavigationText)); + + if (this.ViewState?.ViewedUri is Uri uri) + return uri; + + return ToString(); + } + } } } diff --git a/ILSpy/Themes/ThemeManager.cs b/ILSpy/Themes/ThemeManager.cs index 667fc83e6..3b5b1f8ec 100644 --- a/ILSpy/Themes/ThemeManager.cs +++ b/ILSpy/Themes/ThemeManager.cs @@ -24,6 +24,7 @@ using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; +using System.Windows.Controls.Primitives; using System.Windows.Media; using ICSharpCode.AvalonEdit.Highlighting; @@ -81,6 +82,11 @@ namespace ICSharpCode.ILSpy.Themes return new Style(typeof(Button), (Style)Application.Current.FindResource(ToolBar.ButtonStyleKey)); } + public Style CreateToolBarToggleButtonStyle() + { + return new Style(typeof(ToggleButton), (Style)Application.Current.FindResource(ToolBar.ToggleButtonStyleKey)); + } + public void ApplyHighlightingColors(IHighlightingDefinition highlightingDefinition) { // Make sure all color values are taken from the theme diff --git a/ILSpy/TreeNodes/BaseTypesTreeNode.cs b/ILSpy/TreeNodes/BaseTypesTreeNode.cs index 150408b7f..f0726b432 100644 --- a/ILSpy/TreeNodes/BaseTypesTreeNode.cs +++ b/ILSpy/TreeNodes/BaseTypesTreeNode.cs @@ -46,6 +46,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => Properties.Resources.BaseTypes; + public override object NavigationText => this.Language.TypeToString(type, includeNamespace: true) + " " + Text; + public override object Icon => Images.SuperTypes; protected override void LoadChildren() diff --git a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs index 21b471ea9..5166f0758 100644 --- a/ILSpy/TreeNodes/DerivedTypesTreeNode.cs +++ b/ILSpy/TreeNodes/DerivedTypesTreeNode.cs @@ -47,6 +47,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => Resources.DerivedTypes; + public override object NavigationText => this.Language.TypeToString(type, includeNamespace: true) + " " + Text; + public override object Icon => Images.SubTypes; protected override void LoadChildren() diff --git a/ILSpy/TreeNodes/EventTreeNode.cs b/ILSpy/TreeNodes/EventTreeNode.cs index b03d16d4d..1c10674e9 100644 --- a/ILSpy/TreeNodes/EventTreeNode.cs +++ b/ILSpy/TreeNodes/EventTreeNode.cs @@ -49,6 +49,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => GetText(GetEventDefinition(), this.Language) + GetSuffixString(EventDefinition); + public override object NavigationText => GetText(GetEventDefinition(), Language, includeDeclaringTypeName: true); + private IEvent GetEventDefinition() { return ((MetadataModule)EventDefinition.ParentModule?.MetadataFile @@ -56,9 +58,9 @@ namespace ICSharpCode.ILSpy.TreeNodes ?.MainModule)?.GetDefinition((EventDefinitionHandle)EventDefinition.MetadataToken) ?? EventDefinition; } - public static object GetText(IEvent ev, Language language) + public static object GetText(IEvent ev, Language language, bool includeDeclaringTypeName = false) { - return language.EventToString(ev, false, false, false); + return language.EventToString(ev, includeDeclaringTypeName, false, false); } public override object Icon => GetIcon(GetEventDefinition()); diff --git a/ILSpy/TreeNodes/FieldTreeNode.cs b/ILSpy/TreeNodes/FieldTreeNode.cs index ffd74e774..1cf4b17b2 100644 --- a/ILSpy/TreeNodes/FieldTreeNode.cs +++ b/ILSpy/TreeNodes/FieldTreeNode.cs @@ -41,6 +41,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => GetText(GetFieldDefinition(), Language) + GetSuffixString(FieldDefinition); + public override object NavigationText => GetText(GetFieldDefinition(), Language, includeDeclaringTypeName: true); + private IField GetFieldDefinition() { return ((MetadataModule)FieldDefinition.ParentModule?.MetadataFile @@ -48,9 +50,9 @@ namespace ICSharpCode.ILSpy.TreeNodes ?.MainModule)?.GetDefinition((FieldDefinitionHandle)FieldDefinition.MetadataToken) ?? FieldDefinition; } - public static object GetText(IField field, Language language) + public static object GetText(IField field, Language language, bool includeDeclaringTypeName = false) { - return language.FieldToString(field, includeDeclaringTypeName: false, includeNamespace: false, includeNamespaceOfDeclaringTypeName: false); + return language.FieldToString(field, includeDeclaringTypeName, includeNamespace: false, includeNamespaceOfDeclaringTypeName: false); } public override object Icon => GetIcon(GetFieldDefinition()); diff --git a/ILSpy/TreeNodes/MethodTreeNode.cs b/ILSpy/TreeNodes/MethodTreeNode.cs index c8522887e..fc6d44ce6 100644 --- a/ILSpy/TreeNodes/MethodTreeNode.cs +++ b/ILSpy/TreeNodes/MethodTreeNode.cs @@ -41,6 +41,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => GetText(GetMethodDefinition(), Language) + GetSuffixString(MethodDefinition); + public override object NavigationText => GetText(GetMethodDefinition(), Language, includeDeclaringTypeName: true); + private IMethod GetMethodDefinition() { return ((MetadataModule)MethodDefinition.ParentModule?.MetadataFile @@ -48,9 +50,9 @@ namespace ICSharpCode.ILSpy.TreeNodes ?.MainModule)?.GetDefinition((MethodDefinitionHandle)MethodDefinition.MetadataToken) ?? MethodDefinition; } - public static object GetText(IMethod method, Language language) + public static object GetText(IMethod method, Language language, bool includeDeclaringTypeName = false) { - return language.MethodToString(method, false, false, false); + return language.MethodToString(method, includeDeclaringTypeName, false, false); } public override object Icon => GetIcon(GetMethodDefinition()); diff --git a/ILSpy/TreeNodes/PropertyTreeNode.cs b/ILSpy/TreeNodes/PropertyTreeNode.cs index 12d689d18..255299f8c 100644 --- a/ILSpy/TreeNodes/PropertyTreeNode.cs +++ b/ILSpy/TreeNodes/PropertyTreeNode.cs @@ -51,6 +51,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => GetText(GetPropertyDefinition(), Language) + GetSuffixString(PropertyDefinition); + public override object NavigationText => GetText(GetPropertyDefinition(), Language, includeDeclaringTypeName: true); + private IProperty GetPropertyDefinition() { return ((MetadataModule)PropertyDefinition.ParentModule?.MetadataFile @@ -58,9 +60,9 @@ namespace ICSharpCode.ILSpy.TreeNodes ?.MainModule)?.GetDefinition((PropertyDefinitionHandle)PropertyDefinition.MetadataToken) ?? PropertyDefinition; } - public static object GetText(IProperty property, Language language) + public static object GetText(IProperty property, Language language, bool includeDeclaringTypeName = false) { - return language.PropertyToString(property, false, false, false); + return language.PropertyToString(property, includeDeclaringTypeName, false, false); } public override object Icon => GetIcon(GetPropertyDefinition()); diff --git a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs index 6957ea634..2503c6ad3 100644 --- a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs +++ b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs @@ -44,6 +44,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => Resources.References; + public override object NavigationText => module.Name + " " + Text; + public override object Icon => Images.ReferenceFolder; protected override void LoadChildren() diff --git a/ILSpy/TreeNodes/ResourceListTreeNode.cs b/ILSpy/TreeNodes/ResourceListTreeNode.cs index 9ce35eaf7..f461cf906 100644 --- a/ILSpy/TreeNodes/ResourceListTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceListTreeNode.cs @@ -41,6 +41,8 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => Resources._Resources; + public override object NavigationText => module.Name + " " + Text; + public override object Icon => Images.FolderClosed; public override object ExpandedIcon => Images.FolderOpen; diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index d2e7bdc31..343f8f3df 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -45,6 +45,9 @@ namespace ICSharpCode.ILSpy.TreeNodes public override object Text => this.Language.TypeToString(GetTypeDefinition(), includeNamespace: false) + GetSuffixString(TypeDefinition.MetadataToken); + public override object NavigationText => this.Language.TypeToString(GetTypeDefinition(), includeNamespace: true) + + GetSuffixString(TypeDefinition.MetadataToken); + private ITypeDefinition GetTypeDefinition() { return ((MetadataModule)ParentAssemblyNode.LoadedAssembly