diff --git a/ReClass.NET/Constants.cs b/ReClass.NET/Constants.cs index 4e54782a..bd918dbe 100644 --- a/ReClass.NET/Constants.cs +++ b/ReClass.NET/Constants.cs @@ -1,4 +1,4 @@ -namespace ReClassNET +namespace ReClassNET { public class Constants { @@ -39,5 +39,14 @@ public static class CommandLineOptions public const string FileExtRegister = "registerfileext"; public const string FileExtUnregister = "unregisterfileext"; } + + /// + /// Change type for commandified members in classes which is used to signal what change occurred exactly. As we don't use this feature of the commandified + /// class, this enum is defined to simply signal 'no specific change other than it changed' happened. + /// + public enum GeneralPurposeChangeType + { + None + } } } diff --git a/ReClass.NET/Controls/MemoryViewControl.cs b/ReClass.NET/Controls/MemoryViewControl.cs index 64b8dfa4..d323903b 100644 --- a/ReClass.NET/Controls/MemoryViewControl.cs +++ b/ReClass.NET/Controls/MemoryViewControl.cs @@ -704,5 +704,23 @@ public void Reset() VerticalScroll.Value = VerticalScroll.Minimum; } + + public void InitCurrentClassFromRTTI(ClassNode classNode) + { + var args = new DrawContextRequestEventArgs { Node = classNode }; + + var requestHandler = DrawContextRequested; + requestHandler?.Invoke(this, args); + var view = new DrawContext + { + Settings = args.Settings, + Process = args.Process, + Memory = args.Memory, + CurrentTime = args.CurrentTime, + Address = args.BaseAddress, + Level = 0, + }; + classNode.InitFromRTTI(view); + } } } diff --git a/ReClass.NET/Forms/MainForm.Designer.cs b/ReClass.NET/Forms/MainForm.Designer.cs index e2b2a199..20b2c907 100644 --- a/ReClass.NET/Forms/MainForm.Designer.cs +++ b/ReClass.NET/Forms/MainForm.Designer.cs @@ -77,6 +77,7 @@ private void InitializeComponent() this.toolStripSeparator8 = new System.Windows.Forms.ToolStripSeparator(); this.createClassFromNodesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator13 = new System.Windows.Forms.ToolStripSeparator(); + this.initClassToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.dissectNodesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator9 = new System.Windows.Forms.ToolStripSeparator(); this.searchForEqualValuesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -106,6 +107,7 @@ private void InitializeComponent() this.saveToolStripButton = new System.Windows.Forms.ToolStripButton(); this.toolStripSeparator7 = new System.Windows.Forms.ToolStripSeparator(); this.newClassToolStripButton = new System.Windows.Forms.ToolStripButton(); + this.initClassFromRTTIToolStripBarMenuItem = new ReClassNET.Controls.TypeToolStripMenuItem(); this.addBytesToolStripDropDownButton = new System.Windows.Forms.ToolStripDropDownButton(); this.add4BytesToolStripMenuItem = new ReClassNET.Controls.IntegerToolStripMenuItem(); this.add8BytesToolStripMenuItem = new ReClassNET.Controls.IntegerToolStripMenuItem(); @@ -167,6 +169,8 @@ private void InitializeComponent() this.generateCSharpCodeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.helpToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.undoToolbarMenuItem = new ReClassNET.Controls.TypeToolStripMenuItem(); + this.redoToolbarMenuItem = new ReClassNET.Controls.TypeToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); this.splitContainer.Panel1.SuspendLayout(); this.splitContainer.Panel2.SuspendLayout(); @@ -202,7 +206,7 @@ private void InitializeComponent() // this.splitContainer.Panel2.BackColor = System.Drawing.SystemColors.Control; this.splitContainer.Panel2.Controls.Add(this.memoryViewControl); - this.splitContainer.Size = new System.Drawing.Size(1141, 524); + this.splitContainer.Size = new System.Drawing.Size(1103, 524); this.splitContainer.SplitterDistance = 201; this.splitContainer.TabIndex = 4; // @@ -364,7 +368,7 @@ private void InitializeComponent() this.memoryViewControl.Location = new System.Drawing.Point(0, 0); this.memoryViewControl.Name = "memoryViewControl"; this.memoryViewControl.NodeContextMenuStrip = this.selectedNodeContextMenuStrip; - this.memoryViewControl.Size = new System.Drawing.Size(936, 524); + this.memoryViewControl.Size = new System.Drawing.Size(898, 524); this.memoryViewControl.TabIndex = 0; this.memoryViewControl.DrawContextRequested += new ReClassNET.Controls.DrawContextRequestEventHandler(this.memoryViewControl_DrawContextRequested); this.memoryViewControl.SelectionChanged += new System.EventHandler(this.memoryViewControl_SelectionChanged); @@ -382,6 +386,7 @@ private void InitializeComponent() this.toolStripSeparator8, this.createClassFromNodesToolStripMenuItem, this.toolStripSeparator13, + this.initClassToolStripMenuItem, this.dissectNodesToolStripMenuItem, this.toolStripSeparator9, this.searchForEqualValuesToolStripMenuItem, @@ -402,7 +407,7 @@ private void InitializeComponent() this.showCodeOfClassToolStripMenuItem, this.shrinkClassToolStripMenuItem}); this.selectedNodeContextMenuStrip.Name = "selectedNodeContextMenuStrip"; - this.selectedNodeContextMenuStrip.Size = new System.Drawing.Size(270, 410); + this.selectedNodeContextMenuStrip.Size = new System.Drawing.Size(270, 432); this.selectedNodeContextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(this.selectedNodeContextMenuStrip_Opening); // // changeTypeToolStripMenuItem @@ -604,6 +609,16 @@ private void InitializeComponent() this.toolStripSeparator13.Name = "toolStripSeparator13"; this.toolStripSeparator13.Size = new System.Drawing.Size(266, 6); // + // initClassToolStripMenuItem + // + this.initClassToolStripMenuItem.Image = global::ReClassNET.Properties.Resources.B16x16_Button_AutoName; + this.initClassToolStripMenuItem.Name = "initClassToolStripMenuItem"; + this.initClassToolStripMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Shift) + | System.Windows.Forms.Keys.N))); + this.initClassToolStripMenuItem.Size = new System.Drawing.Size(269, 22); + this.initClassToolStripMenuItem.Text = "Init Class from RTTI"; + this.initClassToolStripMenuItem.Click += new System.EventHandler(this.initClassToolStripMenuItem_Click); + // // dissectNodesToolStripMenuItem // this.dissectNodesToolStripMenuItem.Image = global::ReClassNET.Properties.Resources.B16x16_Camera; @@ -771,12 +786,15 @@ private void InitializeComponent() this.saveToolStripButton, this.toolStripSeparator7, this.newClassToolStripButton, + this.initClassFromRTTIToolStripBarMenuItem, this.addBytesToolStripDropDownButton, this.insertBytesToolStripDropDownButton, - this.nodeTypesToolStripSeparator}); + this.nodeTypesToolStripSeparator, + this.undoToolbarMenuItem, + this.redoToolbarMenuItem}); this.toolStrip.Location = new System.Drawing.Point(0, 24); this.toolStrip.Name = "toolStrip"; - this.toolStrip.Size = new System.Drawing.Size(1141, 25); + this.toolStrip.Size = new System.Drawing.Size(1103, 25); this.toolStrip.TabIndex = 3; // // attachToProcessToolStripSplitButton @@ -832,6 +850,19 @@ private void InitializeComponent() this.newClassToolStripButton.ToolTipText = "Add a new class to this project"; this.newClassToolStripButton.Click += new System.EventHandler(this.newClassToolStripButton_Click); // + // initClassFromRTTIToolStripBarMenuItem + // + this.initClassFromRTTIToolStripBarMenuItem.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; + this.initClassFromRTTIToolStripBarMenuItem.Image = global::ReClassNET.Properties.Resources.B16x16_Button_AutoName; + this.initClassFromRTTIToolStripBarMenuItem.Name = "initClassFromRTTIToolStripBarMenuItem"; + this.initClassFromRTTIToolStripBarMenuItem.Overflow = System.Windows.Forms.ToolStripItemOverflow.AsNeeded; + this.initClassFromRTTIToolStripBarMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)(((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Shift) + | System.Windows.Forms.Keys.N))); + this.initClassFromRTTIToolStripBarMenuItem.Size = new System.Drawing.Size(28, 25); + this.initClassFromRTTIToolStripBarMenuItem.ToolTipText = "Init selected class from RTTI info"; + this.initClassFromRTTIToolStripBarMenuItem.Value = null; + this.initClassFromRTTIToolStripBarMenuItem.Click += new System.EventHandler(this.initClassToolStripMenuItem_Click); + // // addBytesToolStripDropDownButton // this.addBytesToolStripDropDownButton.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image; @@ -1023,7 +1054,7 @@ private void InitializeComponent() this.infoToolStripStatusLabel}); this.statusStrip.Location = new System.Drawing.Point(0, 573); this.statusStrip.Name = "statusStrip"; - this.statusStrip.Size = new System.Drawing.Size(1141, 22); + this.statusStrip.Size = new System.Drawing.Size(1103, 22); this.statusStrip.TabIndex = 1; // // processInfoToolStripStatusLabel @@ -1048,7 +1079,7 @@ private void InitializeComponent() this.helpToolStripMenuItem}); this.mainMenuStrip.Location = new System.Drawing.Point(0, 0); this.mainMenuStrip.Name = "mainMenuStrip"; - this.mainMenuStrip.Size = new System.Drawing.Size(1141, 24); + this.mainMenuStrip.Size = new System.Drawing.Size(1103, 24); this.mainMenuStrip.TabIndex = 2; // // fileToolStripMenuItem @@ -1367,16 +1398,37 @@ private void InitializeComponent() this.aboutToolStripMenuItem.Text = "About..."; this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click); // + // undoToolbarMenuItem + // + this.undoToolbarMenuItem.Image = global::ReClassNET.Properties.Resources.B16x16_Undo; + this.undoToolbarMenuItem.Name = "undoToolbarMenuItem"; + this.undoToolbarMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Z))); + this.undoToolbarMenuItem.Size = new System.Drawing.Size(28, 25); + this.undoToolbarMenuItem.ToolTipText = "Undo the latest change"; + this.undoToolbarMenuItem.Value = null; + this.undoToolbarMenuItem.Click += new System.EventHandler(this.undoToolbarMenuItem_Click); + // + // redoToolbarMenuItem + // + this.redoToolbarMenuItem.Image = global::ReClassNET.Properties.Resources.B16x16_Redo; + this.redoToolbarMenuItem.Name = "redoToolbarMenuItem"; + this.redoToolbarMenuItem.ShortcutKeys = ((System.Windows.Forms.Keys)((System.Windows.Forms.Keys.Control | System.Windows.Forms.Keys.Y))); + this.redoToolbarMenuItem.Size = new System.Drawing.Size(28, 25); + this.redoToolbarMenuItem.ToolTipText = "Redo the latest undone change"; + this.redoToolbarMenuItem.Value = null; + this.redoToolbarMenuItem.Click += new System.EventHandler(this.redoToolbarMenuItem_Click); + // // MainForm // this.AllowDrop = true; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(1141, 595); + this.ClientSize = new System.Drawing.Size(1103, 595); this.Controls.Add(this.splitContainer); this.Controls.Add(this.toolStrip); this.Controls.Add(this.statusStrip); this.Controls.Add(this.mainMenuStrip); + this.KeyPreview = true; this.MainMenuStrip = this.mainMenuStrip; this.MinimumSize = new System.Drawing.Size(200, 100); this.Name = "MainForm"; @@ -1542,6 +1594,10 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem showEnumsToolStripMenuItem; private System.Windows.Forms.ToolStripSeparator toolStripSeparator23; private System.Windows.Forms.ToolStripMenuItem isLittleEndianToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem initClassToolStripMenuItem; + private TypeToolStripMenuItem initClassFromRTTIToolStripBarMenuItem; + private TypeToolStripMenuItem undoToolbarMenuItem; + private TypeToolStripMenuItem redoToolbarMenuItem; } } diff --git a/ReClass.NET/Forms/MainForm.Functions.cs b/ReClass.NET/Forms/MainForm.Functions.cs index 84f1b53a..d5eaeeab 100644 --- a/ReClass.NET/Forms/MainForm.Functions.cs +++ b/ReClass.NET/Forms/MainForm.Functions.cs @@ -16,6 +16,7 @@ using ReClassNET.Nodes; using ReClassNET.Project; using ReClassNET.UI; +using SD.Tools.Algorithmia.Commands; namespace ReClassNET.Forms { @@ -213,6 +214,10 @@ public void LoadProjectFromPath(string path) { Contract.Requires(path != null); + CommandQueueManagerSingleton.GetInstance().ResetActiveCommandQueue(); + CommandQueueManagerSingleton.GetInstance().BeginNonUndoablePeriod(); // we don't want to trigger undo/redo activity while loading + CommandQueueManagerSingleton.GetInstance().RaiseEvents = false; + var project = new ReClassNetProject(); LoadProjectFromPath(path, ref project); @@ -224,6 +229,10 @@ public void LoadProjectFromPath(string path) } SetProject(project); + + // Done loading, resume undo/redo activity + CommandQueueManagerSingleton.GetInstance().RaiseEvents = true; + CommandQueueManagerSingleton.GetInstance().EndNonUndoablePeriod(); } /// Loads the file into the given project. @@ -310,6 +319,9 @@ public void ReplaceSelectedNodesWithType(Type type) { var selected = hotSpotsToReplace.Dequeue(); + // Use a single command here to wrap all state changes into one single undoable object, so everything gets undone/redone in 1 go + var cmd = new UndoablePeriodCommand("Replace node"); + CommandQueueManagerSingleton.GetInstance().BeginUndoablePeriod(cmd); var node = BaseNode.CreateInstanceFromType(type); var createdNodes = new List(); @@ -329,6 +341,8 @@ public void ReplaceSelectedNodesWithType(Type type) hotSpotsToReplace.Enqueue(new MemoryViewControl.SelectedNodeInfo(createdNode, selected.Process, selected.Memory, selected.Address + createdNode.Offset - node.Offset, selected.Level)); } } + // Mark the end of the activities that have to be tracked with this single command + CommandQueueManagerSingleton.GetInstance().EndUndoablePeriod(cmd); } } diff --git a/ReClass.NET/Forms/MainForm.cs b/ReClass.NET/Forms/MainForm.cs index e771a10d..53b1c8db 100644 --- a/ReClass.NET/Forms/MainForm.cs +++ b/ReClass.NET/Forms/MainForm.cs @@ -22,6 +22,7 @@ using ReClassNET.UI; using ReClassNET.Util; using ReClassNET.Util.Conversion; +using SD.Tools.Algorithmia.Commands; namespace ReClassNET.Forms { @@ -95,8 +96,11 @@ public MainForm() }; pluginManager = new PluginManager(new DefaultPluginHost(this, Program.RemoteProcess, Program.Logger)); + + CommandQueueManagerSingleton.GetInstance().CommandQueueActionPerformed += OnCommandQueueActionPerformed; } + protected override void OnLoad(EventArgs e) { base.OnLoad(e); @@ -135,6 +139,8 @@ protected override void OnLoad(EventArgs e) { AttachToProcess(Program.CommandLineArgs[Constants.CommandLineOptions.AttachTo]); } + + SetStateOfUndoRedoButtons(); } protected override void OnFormClosed(FormClosedEventArgs e) @@ -835,6 +841,8 @@ private void memoryViewControl_SelectionChanged(object sender, EventArgs e) addBytesToolStripDropDownButton.Enabled = parentContainer != null || isContainerNode; insertBytesToolStripDropDownButton.Enabled = selectedNodes.Count == 1 && parentContainer != null && !isContainerNode; + initClassToolStripMenuItem.Enabled = nodeIsClass; + initClassFromRTTIToolStripBarMenuItem.Enabled = nodeIsClass; var enabled = selectedNodes.Count > 0 && !nodeIsClass; toolStrip.Items.OfType().ForEach(b => b.Enabled = enabled); @@ -1027,7 +1035,7 @@ private void memoryViewControl_DrawContextRequested(object sender, DrawContextRe { var process = Program.RemoteProcess; - var classNode = CurrentClassNode; + var classNode = (args.Node as ClassNode) ?? CurrentClassNode; if (classNode != null) { memoryViewBuffer.Size = classNode.MemorySize; @@ -1051,5 +1059,45 @@ private void memoryViewControl_DrawContextRequested(object sender, DrawContextRe args.BaseAddress = address; } } + + private void initClassToolStripMenuItem_Click(object sender, EventArgs e) + { + var selectedNodes = memoryViewControl.GetSelectedNodes(); + var node = selectedNodes.FirstOrDefault()?.Node; + if (node == null || !(node is ClassNode)) + { + return; + } + + var cmd = new UndoablePeriodCommand("InitClassFromRTTI"); + CommandQueueManagerSingleton.GetInstance().BeginUndoablePeriod(cmd); + memoryViewControl.InitCurrentClassFromRTTI(node as ClassNode); + CommandQueueManagerSingleton.GetInstance().EndUndoablePeriod(cmd); + } + + + private void SetStateOfUndoRedoButtons() + { + undoToolbarMenuItem.Enabled = CommandQueueManagerSingleton.GetInstance().CanUndo(Program.CommandQueueID); + redoToolbarMenuItem.Enabled = CommandQueueManagerSingleton.GetInstance().CanDo(Program.CommandQueueID); + } + + + private void OnCommandQueueActionPerformed(object sender, CommandQueueActionPerformedEventArgs e) + { + SetStateOfUndoRedoButtons(); + } + + + private void undoToolbarMenuItem_Click(object sender, EventArgs e) + { + CommandQueueManagerSingleton.GetInstance().UndoLastCommand(); + } + + + private void redoToolbarMenuItem_Click(object sender, EventArgs e) + { + CommandQueueManagerSingleton.GetInstance().RedoLastCommand(); + } } } diff --git a/ReClass.NET/Forms/MainForm.resx b/ReClass.NET/Forms/MainForm.resx index c430dab3..0e88c503 100644 --- a/ReClass.NET/Forms/MainForm.resx +++ b/ReClass.NET/Forms/MainForm.resx @@ -206,6 +206,6 @@ - 42 + 104 \ No newline at end of file diff --git a/ReClass.NET/Memory/MemoryBuffer.cs b/ReClass.NET/Memory/MemoryBuffer.cs index e1b515a4..cb7361ec 100644 --- a/ReClass.NET/Memory/MemoryBuffer.cs +++ b/ReClass.NET/Memory/MemoryBuffer.cs @@ -366,5 +366,17 @@ public bool HasChanged(int offset, int length) return false; } + + public UInt64FloatDoubleData InterpretData64(int offset) => new UInt64FloatDoubleData + { + Raw1 = ReadInt32(offset), + Raw2 = ReadInt32(offset + sizeof(int)) + }; + + + public UInt32FloatData InterpretData32(int offset) => new UInt32FloatData + { + Raw = ReadInt32(offset) + }; } } diff --git a/ReClass.NET/Nodes/BaseContainerNode.cs b/ReClass.NET/Nodes/BaseContainerNode.cs index 6926111f..1d3ec5e8 100644 --- a/ReClass.NET/Nodes/BaseContainerNode.cs +++ b/ReClass.NET/Nodes/BaseContainerNode.cs @@ -1,12 +1,16 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; +using ReClassNET.Extensions; +using SD.Tools.Algorithmia.Commands; +using SD.Tools.Algorithmia.GeneralDataStructures; namespace ReClassNET.Nodes { public abstract class BaseContainerNode : BaseNode { - private readonly List nodes = new List(); + //private readonly List nodes = new List(); + private readonly CommandifiedList nodes = new CommandifiedList(); private int updateCount; @@ -183,8 +187,8 @@ public void ReplaceChildNode(BaseNode oldNode, BaseNode newNode, ref List createdNodes) { return; } - + // Mark the actions that follow as actions that have to be ignored so they're not ending up in a command's command queue + CommandQueueManagerSingleton.GetInstance().BeginNonUndoablePeriod(); while (size > 0) { var node = CreateDefaultNodeForSize(size); @@ -281,6 +286,8 @@ protected void InsertBytes(int index, int size, ref List createdNodes) index++; } + // Mark the end of the actions that have to be ignored for undo/redo + CommandQueueManagerSingleton.GetInstance().EndNonUndoablePeriod(); OnNodesUpdated(); } diff --git a/ReClass.NET/Nodes/BaseHexCommentNode.cs b/ReClass.NET/Nodes/BaseHexCommentNode.cs index f2a4053f..98305d8c 100644 --- a/ReClass.NET/Nodes/BaseHexCommentNode.cs +++ b/ReClass.NET/Nodes/BaseHexCommentNode.cs @@ -44,7 +44,7 @@ protected int AddComment(DrawContext view, int x, int y, float fvalue, IntPtr iv if (view.Settings.ShowCommentRtti) { - var rtti = view.Process.ReadRemoteRuntimeTypeInformation(ivalue); + var rtti = GetAssociatedRemoteRuntimeTypeInformation(view, ivalue); if (!string.IsNullOrEmpty(rtti)) { x = AddText(view, x, y, view.Settings.OffsetColor, HotSpot.ReadOnlyId, rtti) + view.Font.Width; @@ -110,5 +110,10 @@ protected int AddComment(DrawContext view, int x, int y, float fvalue, IntPtr iv return x; } + + public string GetAssociatedRemoteRuntimeTypeInformation(DrawContext context, IntPtr ivalue) + { + return context.Process.ReadRemoteRuntimeTypeInformation(ivalue); + } } } diff --git a/ReClass.NET/Nodes/BaseNode.cs b/ReClass.NET/Nodes/BaseNode.cs index adef39e5..edac2e11 100644 --- a/ReClass.NET/Nodes/BaseNode.cs +++ b/ReClass.NET/Nodes/BaseNode.cs @@ -7,6 +7,8 @@ using ReClassNET.Extensions; using ReClassNET.UI; using ReClassNET.Util; +using SD.Tools.Algorithmia.GeneralDataStructures; +using SD.Tools.Algorithmia.GeneralDataStructures.EventArguments; namespace ReClassNET.Nodes { @@ -24,14 +26,25 @@ public abstract class BaseNode private static int nodeIndex = 0; - private string name = string.Empty; + private CommandifiedMember name; private string comment = string.Empty; /// Gets or sets the offset of the node. public int Offset { get; set; } /// Gets or sets the name of the node. If a new name was set the property changed event gets fired. - public virtual string Name { get => name; set { if (value != null && name != value) { name = value; NameChanged?.Invoke(this); } } } + public virtual string Name + { + get => name.MemberValue; + set + { + if (value == null) + { + return; + } + name.MemberValue = value; + } + } /// Gets or sets the comment of the node. public string Comment { get => comment; set { if (value != null && comment != value) { comment = value; CommentChanged?.Invoke(this); } } } @@ -39,9 +52,12 @@ public abstract class BaseNode /// Gets or sets the parent node. public BaseNode ParentNode { get; internal set; } - /// Gets a value indicating whether this node is wrapped into an other node. + /// Gets a value indicating whether this node is wrapped into an other node. public bool IsWrapped => ParentNode is BaseWrapperNode; + /// All nodes that are wrapped can't be selected except classnodes because they have a context menu + public bool CanBeSelected => !IsWrapped || (this is ClassNode); + /// Gets or sets a value indicating whether this node is hidden. public bool IsHidden { get; set; } @@ -97,12 +113,15 @@ protected BaseNode() Contract.Ensures(name != null); Contract.Ensures(comment != null); - Name = $"N{nodeIndex++:X08}"; + name = new CommandifiedMember("Name", Constants.GeneralPurposeChangeType.None, $"N{nodeIndex++:X08}"); + name.ValueChanged += Name_ValueChanged; Comment = string.Empty; LevelsOpen[0] = true; } + private void Name_ValueChanged(object sender, MemberChangedEventArgs e) => NameChanged?.Invoke(this); + public abstract void GetUserInterfaceInfo(out string name, out Image icon); public virtual bool UseMemoryPreviewToolTip(HotSpot spot, out IntPtr address) @@ -236,6 +255,15 @@ public virtual void ClearSelection() /// The calculated height. public abstract int CalculateDrawnHeight(DrawContext context); + /// + /// Called when this node has been created, initialized and the parent node has been assigned. For some nodes + /// Additional work has to be performed, this work can be done in a derived method of this method. + /// + public virtual void PerformPostInitWork() + { + // nop + } + /// Updates the node from the given . Sets the and of the node. /// The spot. public virtual void Update(HotSpot spot) @@ -367,7 +395,7 @@ protected void AddSelection(DrawContext context, int x, int y, int height) Contract.Requires(context != null); Contract.Requires(context.Graphics != null); - if (y > context.ClientArea.Bottom || y + height < 0 || IsWrapped) + if (y > context.ClientArea.Bottom || y + height < 0 || !CanBeSelected) { return; } diff --git a/ReClass.NET/Nodes/ClassNode.cs b/ReClass.NET/Nodes/ClassNode.cs index 9b144061..d5eba916 100644 --- a/ReClass.NET/Nodes/ClassNode.cs +++ b/ReClass.NET/Nodes/ClassNode.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Drawing; using System.Linq; using ReClassNET.Controls; using ReClassNET.UI; +using SD.Tools.Algorithmia.GeneralDataStructures; +using SD.Tools.Algorithmia.GeneralDataStructures.EventArguments; namespace ReClassNET.Nodes { @@ -27,7 +30,12 @@ public class ClassNode : BaseContainerNode public Guid Uuid { get; set; } - public string AddressFormula { get; set; } = DefaultAddressFormula; + private CommandifiedMember addressFormula; + public string AddressFormula + { + get => addressFormula.MemberValue; + set => addressFormula.MemberValue = value; + } public event NodeEventHandler NodesChanged; @@ -35,6 +43,7 @@ internal ClassNode(bool notifyClassCreated) { Contract.Ensures(AddressFormula != null); + addressFormula = new CommandifiedMember("AddressFormula", Constants.GeneralPurposeChangeType.None, DefaultAddressFormula); LevelsOpen.DefaultValue = true; Uuid = Guid.NewGuid(); @@ -51,7 +60,48 @@ public static ClassNode Create() return new ClassNode(true); } + + /// + /// Initializes the class' name and vtable node from RTTI information, if it's not set already + /// + /// + public void InitFromRTTI(DrawContext context) + { + // first node should be a VTable node or a hex64/32 node + if (Nodes.Count <= 0) + { + return; + } + + var rttiInfoFromFirstNode = string.Empty; + var firstNode = Nodes[0]; + if (firstNode is VirtualMethodTableNode vtableNode) + { + rttiInfoFromFirstNode = vtableNode.GetAssociatedRemoteRuntimeTypeInformation(context); + } + else if (firstNode is BaseHexCommentNode baseHexCommentNode) + { + // ask it as if it might point to a vtable + var value = context.Memory.InterpretData64(Offset); + rttiInfoFromFirstNode = baseHexCommentNode.GetAssociatedRemoteRuntimeTypeInformation(context, value.IntPtr); + if (!string.IsNullOrEmpty(rttiInfoFromFirstNode)) + { + // convert first node to vtable node + var newVTableNode = BaseNode.CreateInstanceFromType(typeof(VirtualMethodTableNode)); + var createdNodes = new List(); + this.ReplaceChildNode(firstNode, newVTableNode, ref createdNodes); + } + } + if (string.IsNullOrEmpty(rttiInfoFromFirstNode)) + { + return; + } + + var fragments = rttiInfoFromFirstNode.Split(':'); + this.Name = fragments[0]; + } + public override void GetUserInterfaceInfo(out string name, out Image icon) { throw new InvalidOperationException($"The '{nameof(ClassNode)}' node should not be accessible from the ui."); diff --git a/ReClass.NET/Nodes/Hex32Node.cs b/ReClass.NET/Nodes/Hex32Node.cs index c7b54027..2329c954 100644 --- a/ReClass.NET/Nodes/Hex32Node.cs +++ b/ReClass.NET/Nodes/Hex32Node.cs @@ -18,7 +18,7 @@ public override void GetUserInterfaceInfo(out string name, out Image icon) public override bool UseMemoryPreviewToolTip(HotSpot spot, out IntPtr address) { - var value = ReadFromBuffer(spot.Memory, Offset); + var value = spot.Memory.InterpretData32(Offset); address = value.IntPtr; @@ -27,7 +27,7 @@ public override bool UseMemoryPreviewToolTip(HotSpot spot, out IntPtr address) public override string GetToolTipText(HotSpot spot) { - var value = ReadFromBuffer(spot.Memory, Offset); + var value = spot.Memory.InterpretData32(Offset); return $"Int32: {value.IntValue}\nUInt32: 0x{value.UIntValue:X08}\nFloat: {value.FloatValue:0.000}"; } @@ -46,16 +46,11 @@ protected override int AddComment(DrawContext context, int x, int y) { x = base.AddComment(context, x, y); - var value = ReadFromBuffer(context.Memory, Offset); + var value = context.Memory.InterpretData32(Offset); x = AddComment(context, x, y, value.FloatValue, value.IntPtr, value.UIntPtr); return x; } - - private static UInt32FloatData ReadFromBuffer(MemoryBuffer memory, int offset) => new UInt32FloatData - { - Raw = memory.ReadInt32(offset) - }; } } diff --git a/ReClass.NET/Nodes/Hex64Node.cs b/ReClass.NET/Nodes/Hex64Node.cs index d54f1e71..61749e46 100644 --- a/ReClass.NET/Nodes/Hex64Node.cs +++ b/ReClass.NET/Nodes/Hex64Node.cs @@ -18,7 +18,7 @@ public override void GetUserInterfaceInfo(out string name, out Image icon) public override bool UseMemoryPreviewToolTip(HotSpot spot, out IntPtr address) { - var value = ReadFromBuffer(spot.Memory, Offset); + var value = spot.Memory.InterpretData64(Offset); address = value.IntPtr; @@ -27,7 +27,7 @@ public override bool UseMemoryPreviewToolTip(HotSpot spot, out IntPtr address) public override string GetToolTipText(HotSpot spot) { - var value = ReadFromBuffer(spot.Memory, Offset); + var value = spot.Memory.InterpretData64(Offset); return $"Int64: {value.LongValue}\nUInt64: 0x{value.ULongValue:X016}\nFloat: {value.FloatValue:0.000}\nDouble: {value.DoubleValue:0.000}"; } @@ -46,17 +46,11 @@ protected override int AddComment(DrawContext context, int x, int y) { x = base.AddComment(context, x, y); - var value = ReadFromBuffer(context.Memory, Offset); + var value = context.Memory.InterpretData64(Offset); x = AddComment(context, x, y, value.FloatValue, value.IntPtr, value.UIntPtr); return x; } - - private static UInt64FloatDoubleData ReadFromBuffer(MemoryBuffer memory, int offset) => new UInt64FloatDoubleData - { - Raw1 = memory.ReadInt32(offset), - Raw2 = memory.ReadInt32(offset + sizeof(int)) - }; } } diff --git a/ReClass.NET/Nodes/PointerNode.cs b/ReClass.NET/Nodes/PointerNode.cs index 027b0d28..9df3b8b0 100644 --- a/ReClass.NET/Nodes/PointerNode.cs +++ b/ReClass.NET/Nodes/PointerNode.cs @@ -1,5 +1,6 @@ using System; using System.Drawing; +using ReClassNET.AddressParser; using ReClassNET.Controls; using ReClassNET.Memory; using ReClassNET.UI; @@ -134,5 +135,39 @@ public override int CalculateDrawnHeight(DrawContext context) } return height; } + + public override void PerformPostInitWork() + { + base.PerformPostInitWork(); + + var parentClass = ParentNode as ClassNode; + if (parentClass == null) + { + return; + } + + var process = Program.RemoteProcess; + IntPtr address; + try + { + address = process.ParseAddress(parentClass.AddressFormula); + } + catch (ParseException) + { + address = IntPtr.Zero; + } + + var memoryBuffer = new MemoryBuffer() { Size = parentClass.MemorySize}; + memoryBuffer.UpdateFrom(process, address); + var ptr = memoryBuffer.ReadIntPtr(Offset); + + var classNode = ((ClassInstanceNode)InnerNode)?.InnerNode as ClassNode; + if (classNode == null) + { + return; + } + + classNode.AddressFormula = ptr.ToString(Constants.AddressHexFormat); + } } } diff --git a/ReClass.NET/Nodes/VirtualMethodTableNode.cs b/ReClass.NET/Nodes/VirtualMethodTableNode.cs index 9e82ab40..2c9099a0 100644 --- a/ReClass.NET/Nodes/VirtualMethodTableNode.cs +++ b/ReClass.NET/Nodes/VirtualMethodTableNode.cs @@ -33,7 +33,33 @@ public override void Initialize() AddNode(CreateDefaultNodeForSize(IntPtr.Size)); } } + + protected override int AddComment(DrawContext context, int x, int y) + { + x = base.AddComment(context, x, y); + + if (context.Settings.ShowCommentRtti) + { + var rtti = GetAssociatedRemoteRuntimeTypeInformation(context); + if (!string.IsNullOrEmpty(rtti)) + { + x = AddText(context, x, y, context.Settings.OffsetColor, HotSpot.ReadOnlyId, rtti) + context.Font.Width; + } + } + return x; + } + + public string GetAssociatedRemoteRuntimeTypeInformation(DrawContext context) + { + var addressFirstVTableFunction = context.Memory.InterpretData64(Offset).IntPtr; + if (addressFirstVTableFunction != IntPtr.Zero) + { + return context.Process.ReadRemoteRuntimeTypeInformation(addressFirstVTableFunction); + } + return string.Empty; + } + public override Size Draw(DrawContext context, int x, int y) { if (IsHidden && !IsWrapped) diff --git a/ReClass.NET/Program.cs b/ReClass.NET/Program.cs index f28cb0bd..aba0f3f1 100644 --- a/ReClass.NET/Program.cs +++ b/ReClass.NET/Program.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Drawing; using System.Globalization; +using System.Threading; using System.Windows.Forms; using Microsoft.SqlServer.MessageBox; using ReClassNET.Core; @@ -11,6 +12,7 @@ using ReClassNET.Native; using ReClassNET.UI; using ReClassNET.Util; +using SD.Tools.Algorithmia.Commands; namespace ReClassNET { @@ -34,10 +36,18 @@ public static class Program public static FontEx MonoSpaceFont { get; private set; } + public static Guid CommandQueueID { get; private set; } + [STAThread] static void Main(string[] args) { DesignMode = false; // The designer doesn't call Main() + CommandQueueID = Guid.NewGuid(); + + // wire event handlers for unhandled exceptions, so these will be shown using our own method. + Application.SetUnhandledExceptionMode(UnhandledExceptionMode.Automatic, true); + Application.ThreadException += new ThreadExceptionEventHandler(Program.Application_ThreadException); + AppDomain.CurrentDomain.UnhandledException+=new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); CommandLineArgs = new CommandLineArgs(args); @@ -63,6 +73,11 @@ static void Main(string[] args) Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); + // switch is set to false, so Do actions during Undo actions are ignored. + CommandQueueManager.ThrowExceptionOnDoDuringUndo = false; + // activate our command queue stack. We're only changing things from the main thread so we don't need multiple stacks. + CommandQueueManagerSingleton.GetInstance().ActivateCommandQueueStack(CommandQueueID); + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; Settings = SettingsSerializer.Load(); @@ -98,7 +113,17 @@ static void Main(string[] args) SettingsSerializer.Save(Settings); } - + + private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) + { + ShowException(e.Exception); + } + + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + ShowException(e.ExceptionObject as Exception); + } + /// Shows the exception in a special form. /// The exception. public static void ShowException(Exception ex) diff --git a/ReClass.NET/Project/ReClassNetProject.cs b/ReClass.NET/Project/ReClassNetProject.cs index 90f4b5ca..bd2be84b 100644 --- a/ReClass.NET/Project/ReClassNetProject.cs +++ b/ReClass.NET/Project/ReClassNetProject.cs @@ -1,9 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.Contracts; using System.Linq; +using ReClassNET.Extensions; using ReClassNET.Nodes; using ReClassNET.Util; +using SD.Tools.Algorithmia.GeneralDataStructures; +using SD.Tools.Algorithmia.GeneralDataStructures.EventArguments; namespace ReClassNET.Project { @@ -18,7 +22,7 @@ public class ReClassNetProject : IDisposable public event EnumsChangedEvent EnumRemoved; private readonly List enums = new List(); - private readonly List classes = new List(); + private readonly CommandifiedList classes = new CommandifiedList(); // use a commandified list for the set of classes so we get auto undo/redo tracking public IReadOnlyList Enums => enums; @@ -36,6 +40,25 @@ public class ReClassNetProject : IDisposable /// List of data types to use while generating C++ code for nodes. /// public CppTypeMapping TypeMapping { get; } = new CppTypeMapping(); + + public ReClassNetProject() + { + // We're using ListChanged instead of ElementAdding here because ListChanged is also raised when 'Redo' is executed on the list re-adding the already created element. + classes.ListChanged += Classes_ListChanged; + classes.ElementRemoved += Classes_ElementRemoved; + } + + private void Classes_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e) + { + // nothing. The removed event is handled separately because ListChangedType.ItemRemoved doesn't give access to the removed element and ElementRemoved does. + if (e.ListChangedType == ListChangedType.ItemAdded) + { + ClassAdded?.Invoke(classes[e.NewIndex]); + } + } + + private void Classes_ElementRemoved(object sender, CollectionElementRemovedEventArgs e) => ClassRemoved?.Invoke(e.InvolvedElement); + private void Enums_ElementRemoved(object sender, CollectionElementRemovedEventArgs e) => EnumRemoved?.Invoke(e.InvolvedElement); public void Dispose() { @@ -56,7 +79,7 @@ public void AddClass(ClassNode node) node.NodesChanged += NodesChanged_Handler; - ClassAdded?.Invoke(node); + // No need to invoke the ClassAdded event here, as it's automatically raised when the class is added to the commandified list. } public bool ContainsClass(Guid uuid) diff --git a/ReClass.NET/Properties/Resources.Designer.cs b/ReClass.NET/Properties/Resources.Designer.cs index 3d412074..3738ffbc 100644 --- a/ReClass.NET/Properties/Resources.Designer.cs +++ b/ReClass.NET/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace ReClassNET.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -190,6 +190,16 @@ internal static System.Drawing.Bitmap B16x16_Button_Array { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap B16x16_Button_AutoName { + get { + object obj = ResourceManager.GetObject("B16x16_Button_AutoName", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -1371,7 +1381,7 @@ internal static System.Drawing.Bitmap B32x32_Plugin { } /// - /// Looks up a localized string similar to 2020/10/17 09:45:04 + /// Looks up a localized string similar to 2023/07/03 12:55:32 ///. /// internal static string BuildDate { diff --git a/ReClass.NET/Properties/Resources.resx b/ReClass.NET/Properties/Resources.resx index 48c2c826..3cee6c55 100644 --- a/ReClass.NET/Properties/Resources.resx +++ b/ReClass.NET/Properties/Resources.resx @@ -517,4 +517,7 @@ ..\Resources\Images\B16x16_Button_NUInt.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\Images\B16x16_Button_AutoName.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/ReClass.NET/ReClass.NET.csproj b/ReClass.NET/ReClass.NET.csproj index 0c990cc2..3a98a45a 100644 --- a/ReClass.NET/ReClass.NET.csproj +++ b/ReClass.NET/ReClass.NET.csproj @@ -122,6 +122,12 @@ False ..\Dependencies\Microsoft.ExceptionMessageBox.dll + + ..\packages\SD.Tools.Algorithmia.1.4.0\lib\net452\SD.Tools.Algorithmia.dll + + + ..\packages\SD.Tools.BCLExtensions.1.2.2\lib\net452\SD.Tools.BCLExtensions.dll + @@ -608,6 +614,7 @@ Resources.Designer.cs + SettingsSingleFileGenerator Settings.Designer.cs @@ -1023,6 +1030,9 @@ + + + powershell -Command "((Get-Date).ToUniversalTime()).ToString(\"yyyy\/MM\/dd HH:mm:ss\") | Out-File '$(ProjectDir)Resources\BuildDate.txt'" diff --git a/ReClass.NET/Resources/Images/B16x16_Button_AutoName.png b/ReClass.NET/Resources/Images/B16x16_Button_AutoName.png new file mode 100644 index 00000000..bfdd7e64 Binary files /dev/null and b/ReClass.NET/Resources/Images/B16x16_Button_AutoName.png differ diff --git a/ReClass.NET/Settings.cs b/ReClass.NET/Settings.cs index b5d9268b..32eafc3d 100644 --- a/ReClass.NET/Settings.cs +++ b/ReClass.NET/Settings.cs @@ -1,11 +1,36 @@ -using System.Drawing; +using System; +using System.Collections.Generic; +using System.Drawing; using System.Text; +using System.Windows.Forms; +using ReClassNET.Nodes; using ReClassNET.Util; namespace ReClassNET { public class Settings { + private readonly Dictionary _shortcutKeyPerNode; + + public Settings() + { + _shortcutKeyPerNode = new Dictionary + { + { typeof(Hex64Node), Keys.Control | Keys.Shift | Keys.D6 }, + { typeof(ClassInstanceNode), Keys.Control | Keys.Shift | Keys.C }, + { typeof(FloatNode), Keys.Control | Keys.Shift | Keys.F }, + { typeof(Hex8Node), Keys.Control | Keys.Shift | Keys.B }, + { typeof(PointerNode), Keys.Control | Keys.Shift | Keys.P }, + { typeof(Vector2Node), Keys.Control | Keys.Shift | Keys.D2 }, + { typeof(Vector3Node), Keys.Control | Keys.Shift | Keys.D3 }, + { typeof(Vector4Node), Keys.Control | Keys.Shift | Keys.D4 }, + { typeof(VirtualMethodTableNode), Keys.Control | Keys.Shift | Keys.V }, + { typeof(BoolNode), Keys.Control | Keys.Shift | Keys.O }, + { typeof(EnumNode), Keys.Control | Keys.Shift | Keys.E }, + { typeof(Int32Node), Keys.Control | Keys.Shift | Keys.I } + }; + } + // Application Settings public string LastProcess { get; set; } = string.Empty; @@ -75,6 +100,11 @@ public class Settings public Color PluginColor { get; set; } = Color.FromArgb(255, 0, 255); public CustomDataMap CustomData { get; } = new CustomDataMap(); + + public Keys GetShortcutKeyForNodeType(Type nodeType) + { + return !_shortcutKeyPerNode.TryGetValue(nodeType, out var shortcutKeys) ? Keys.None : shortcutKeys; + } public Settings Clone() => MemberwiseClone() as Settings; } diff --git a/ReClass.NET/UI/HotSpot.cs b/ReClass.NET/UI/HotSpot.cs index 3a0d4bc0..27b949d0 100644 --- a/ReClass.NET/UI/HotSpot.cs +++ b/ReClass.NET/UI/HotSpot.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using ReClassNET.Memory; using ReClassNET.Nodes; diff --git a/ReClass.NET/UI/NodeTypesBuilder.cs b/ReClass.NET/UI/NodeTypesBuilder.cs index 8d519022..79d4bfbe 100644 --- a/ReClass.NET/UI/NodeTypesBuilder.cs +++ b/ReClass.NET/UI/NodeTypesBuilder.cs @@ -14,6 +14,7 @@ internal static class NodeTypesBuilder { private static readonly List defaultNodeTypeGroupList = new List(); private static readonly Dictionary> pluginNodeTypes = new Dictionary>(); + private static readonly HashSet nodeTypesWhichCanOverflowInToolbar; static NodeTypesBuilder() { @@ -27,6 +28,9 @@ static NodeTypesBuilder() defaultNodeTypeGroupList.Add(new[] { typeof(PointerNode), typeof(ArrayNode), typeof(UnionNode) }); defaultNodeTypeGroupList.Add(new[] { typeof(ClassInstanceNode) }); defaultNodeTypeGroupList.Add(new[] { typeof(VirtualMethodTableNode), typeof(FunctionNode), typeof(FunctionPtrNode) }); + + // define the node types which can overflow in the toolbar if the window is too narrow. Add types here which aren't used that much + nodeTypesWhichCanOverflowInToolbar = new HashSet { typeof(NIntNode), typeof(NUIntNode), typeof(BitFieldNode), typeof(Utf16TextNode), typeof(Utf16TextPtrNode) } ; } public static void AddPluginNodeGroup(Plugin plugin, IReadOnlyList nodeTypes) @@ -57,14 +61,16 @@ public static IEnumerable CreateToolStripButtons(Action han return CreateToolStripItems(t => { - GetNodeInfoFromType(t, out var label, out var icon); + GetNodeInfoFromType(t, out var label, out var icon, out var shortcutKeys); - var item = new TypeToolStripButton + var item = new TypeToolStripMenuItem { Value = t, ToolTipText = label, DisplayStyle = ToolStripItemDisplayStyle.Image, - Image = icon + Image = icon, + ShortcutKeys = shortcutKeys, + Overflow = nodeTypesWhichCanOverflowInToolbar.Contains(t) ? ToolStripItemOverflow.AsNeeded : ToolStripItemOverflow.Never, }; item.Click += clickHandler; return item; @@ -74,7 +80,7 @@ public static IEnumerable CreateToolStripButtons(Action han Image = p.Icon }, t => { - GetNodeInfoFromType(t, out var label, out var icon); + GetNodeInfoFromType(t, out var label, out var icon, out var shortcutKeys); var item = new TypeToolStripMenuItem { @@ -95,13 +101,14 @@ public static IEnumerable CreateToolStripMenuItems(Action h var items = CreateToolStripItems(t => { - GetNodeInfoFromType(t, out var label, out var icon); + GetNodeInfoFromType(t, out var label, out var icon, out var shortcutKeys); var item = new TypeToolStripMenuItem { Value = t, Text = label, - Image = icon + Image = icon, + ShortcutKeys = shortcutKeys, }; item.Click += clickHandler; return item; @@ -166,10 +173,12 @@ private static IEnumerable CreateToolStripItems(Func + + + + \ No newline at end of file