diff --git a/WitcherScriptMerger/Controls/SMTree.cs b/WitcherScriptMerger/Controls/SMTree.cs index 58881d5..ba0b19b 100644 --- a/WitcherScriptMerger/Controls/SMTree.cs +++ b/WitcherScriptMerger/Controls/SMTree.cs @@ -1,506 +1,505 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Windows.Forms; -using WitcherScriptMerger.FileIndex; -using WitcherScriptMerger.Inventory; - -namespace WitcherScriptMerger.Controls -{ - abstract class SMTree : TreeView - { - #region Types - - public enum LevelType : int - { - Categories, Files, Mods - } - - public class NodeMetadata - { - public string FilePath; - public FileHash FileHash; - public ModFile ModFile; - } - - #endregion - - #region Members - - public static readonly Color FileNodeForeColor = Color.Black; - - public List CategoryNodes => GetNodesAtLevel(LevelType.Categories); - - public List FileNodes => GetNodesAtLevel(LevelType.Files); - - public List ModNodes => GetNodesAtLevel(LevelType.Mods); - - protected TreeNode ClickedNode = null; - protected bool IsUpdating = false; - - Color _clickedNodeForeColor; - - #endregion - - #region Double-buffering - - // From http://stackoverflow.com/a/10364283/1641069 - // Pinvoke: - private const int TVM_SETEXTENDEDSTYLE = 0x1100 + 44; - private const int TVM_GETEXTENDEDSTYLE = 0x1100 + 45; - private const int TVS_EX_DOUBLEBUFFER = 0x0004; - [DllImport("user32.dll")] - private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp); - - protected override void OnHandleCreated(EventArgs e) - { - SendMessage(this.Handle, TVM_SETEXTENDEDSTYLE, (IntPtr)TVS_EX_DOUBLEBUFFER, (IntPtr)TVS_EX_DOUBLEBUFFER); - base.OnHandleCreated(e); - } - - #endregion - - #region Context Menu Members - - protected TreeNode RightClickedNode; - - ContextMenuStrip _contextMenu; - - protected ToolStripRegion ContextOpenRegion; - ToolStripMenuItem _contextOpenModFile = new ToolStripMenuItem(); - ToolStripMenuItem _contextOpenModFileDir = new ToolStripMenuItem(); - ToolStripMenuItem _contextOpenModBundleDir = new ToolStripMenuItem(); - ToolStripMenuItem _contextOpenVanillaFile = new ToolStripMenuItem(); - ToolStripMenuItem _contextOpenVanillaFileDir = new ToolStripMenuItem(); - ToolStripMenuItem _contextCopyPath = new ToolStripMenuItem(); - - protected ToolStripRegion ContextNodeRegion; - - protected ToolStripRegion ContextAllRegion; - ToolStripSeparator _contextAllSeparator = new ToolStripSeparator(); - ToolStripMenuItem _contextExpandAll = new ToolStripMenuItem(); - ToolStripMenuItem _contextCollapseAll = new ToolStripMenuItem(); - protected ToolStripMenuItem ContextSelectAll = new ToolStripMenuItem(); - protected ToolStripMenuItem ContextDeselectAll = new ToolStripMenuItem(); - - #endregion - - public SMTree() - { - InitializeContextMenu(); - TreeViewNodeSorter = new SMTreeSorter(); - } - - protected List GetNodesAtLevel(LevelType level) - { - var nodes = Nodes.Cast(); - for (int i = 0; i < (int)level; ++i) - nodes = nodes.SelectMany(node => node.GetTreeNodes()); - return nodes.ToList(); - } - - public TreeNode GetCategoryNode(ModFileCategory category) - { - return CategoryNodes.FirstOrDefault(node => - category == (ModFileCategory)node.Tag); - } - - public void SetFontBold(LevelType level) - { - BeginUpdate(); - foreach (var node in GetNodesAtLevel(level)) - node.SetFontStyle(FontStyle.Bold); - EndUpdate(); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (e.Control) - { - if (e.KeyCode == Keys.A) - ContextSelectAll_Click(null, null); - else if (e.KeyCode == Keys.D) - ContextDeselectAll_Click(null, null); - } - } - - protected override void OnAfterSelect(TreeViewEventArgs e) - { - base.OnAfterSelect(e); - SelectedNode = null; - } - - protected override void OnMouseDown(MouseEventArgs e) - { - base.OnMouseDown(e); - ClickedNode = GetNodeAt(e.Location); - if (ClickedNode != null) - { - if (!ClickedNode.Bounds.Contains(e.Location)) - ClickedNode = null; - else if (e.Button == MouseButtons.Left) - { - _clickedNodeForeColor = ClickedNode.ForeColor; - ClickedNode.ForeColor = Color.White; - ClickedNode.BackColor = Color.CornflowerBlue; - } - } - - if (e.Button == MouseButtons.Right) - { - BeginUpdate(); - IsUpdating = true; - } - } - - protected override void OnMouseMove(MouseEventArgs e) - { - base.OnMouseMove(e); - if (ClickedNode == null || RightClickedNode != null || e.Button == MouseButtons.Right) - return; - if (ClickedNode.Bounds.Contains(e.Location)) - { - ClickedNode.BackColor = Color.CornflowerBlue; - ClickedNode.ForeColor = Color.White; - } - else - { - ClickedNode.ForeColor = _clickedNodeForeColor; - ClickedNode.BackColor = Color.Transparent; - } - } - - protected override void OnMouseUp(MouseEventArgs e) - { - base.OnMouseUp(e); - - var lastClicked = ClickedNode; - ClickedNode = GetNodeAt(e.Location); - if (ClickedNode != null && - (lastClicked != ClickedNode || !ClickedNode.Bounds.Contains(e.Location))) - ClickedNode = null; - - if (e.Button == MouseButtons.Left) - { - OnLeftMouseUp(e); - if (lastClicked != null && ClickedNode != null) - { - lastClicked.ForeColor = _clickedNodeForeColor; - lastClicked.BackColor = Color.Transparent; - } - ClickedNode = null; - } - else if (e.Button == MouseButtons.Right) - OnRightMouseUp(e); - EndUpdate(); - IsUpdating = false; - } - - protected virtual void OnLeftMouseUp(MouseEventArgs e) - { - if (ClickedNode == null) - return; - if (ClickedNode.SetCheckedIfVisible(!ClickedNode.Checked)) - HandleCheckedChange(); - } - - protected virtual void OnRightMouseUp(MouseEventArgs e) - { - ResetContextItemAvailability(); - SetContextItemAvailability(); - - if (_contextMenu.Items.OfType().Any(item => item.Available)) - { - if (ClickedNode != null) - ClickedNode.BackColor = Color.Gainsboro; - - SetContextMenuSize(); - - _contextMenu.Show(this, e.X, e.Y); - } - } - - protected override void OnAfterCheck(TreeViewEventArgs e) - { - base.OnAfterCheck(e); - if (e.Action != TreeViewAction.Unknown) // Event was triggered programmatically - { - ClickedNode = e.Node; - HandleCheckedChange(); - } - } - - protected abstract void HandleCheckedChange(); - - protected override void OnMouseLeave(EventArgs e) - { - base.OnMouseLeave(e); - - if (IsUpdating) - EndUpdate(); - } - - protected bool IsCategoryNode(TreeNode node) => ((LevelType)node.Level == LevelType.Categories); - - protected bool IsFileNode(TreeNode node) => ((LevelType)node.Level == LevelType.Files); - - protected bool IsModNode(TreeNode node) => ((LevelType)node.Level == LevelType.Mods); - - #region Context Menu - - void InitializeContextMenu() - { - _contextMenu = new ContextMenuStrip(); - - ContextOpenRegion = new ToolStripRegion(_contextMenu as ToolStrip, new ToolStripItem[] - { - _contextCopyPath, - _contextOpenModFile, - _contextOpenModFileDir, - _contextOpenModBundleDir, - _contextOpenVanillaFile, - _contextOpenVanillaFileDir - }); - - ContextNodeRegion = new ToolStripRegion(_contextMenu as ToolStrip, new ToolStripItem[0]); - - ContextAllRegion = new ToolStripRegion(_contextMenu as ToolStrip, new ToolStripItem[] - { - _contextAllSeparator, - ContextSelectAll, - ContextDeselectAll, - _contextExpandAll, - _contextCollapseAll - }); - - // treeContextMenu - _contextMenu.AutoSize = false; - _contextMenu.Name = "treeContextMenu"; - _contextMenu.Size = new Size(239, 390); - _contextMenu.Closing += ContextMenu_Closing; - - // contextOpenModFile - _contextOpenModFile.Name = "contextOpenModFile"; - _contextOpenModFile.Size = new Size(225, 22); - _contextOpenModFile.Text = "Open Mod File"; - _contextOpenModFile.ToolTipText = "Opens this mod's version of the file"; - _contextOpenModFile.Click += ContextOpenFile_Click; - - // contextOpenModFileDir - _contextOpenModFileDir.Name = "contextOpenModFileDir"; - _contextOpenModFileDir.Size = new Size(225, 22); - _contextOpenModFileDir.Text = "Open Mod File Directory"; - _contextOpenModFileDir.ToolTipText = "Opens the location of this mod's version of the file"; - _contextOpenModFileDir.Click += ContextOpenDirectory_Click; - - // contextOpenModBundleDir - _contextOpenModBundleDir.Name = "contextOpenModBundleDir"; - _contextOpenModBundleDir.Size = new Size(225, 22); - _contextOpenModBundleDir.Text = "Open Mod Bundle Directory"; - _contextOpenModBundleDir.ToolTipText = "Opens the location of this mod's bundle file"; - _contextOpenModBundleDir.Click += ContextOpenDirectory_Click; - - // contextOpenVanillaFile - _contextOpenVanillaFile.Name = "contextOpenVanillaFile"; - _contextOpenVanillaFile.Size = new Size(225, 22); - _contextOpenVanillaFile.Text = "Open Vanilla File"; - _contextOpenVanillaFile.ToolTipText = "Opens the unmodded version of the file"; - _contextOpenVanillaFile.Click += ContextOpenVanillaFile_Click; - - // contextOpenVanillaFileDir - _contextOpenVanillaFileDir.Name = "contextOpenVanillaFileDir"; - _contextOpenVanillaFileDir.Size = new Size(225, 22); - _contextOpenVanillaFileDir.Text = "Open Vanilla File Directory"; - _contextOpenVanillaFileDir.ToolTipText = "Opens the location of the unmodded version of the file"; - _contextOpenVanillaFileDir.Click += ContextOpenVanillaDirectory_Click; - - // contextCopyPath - _contextCopyPath.Name = "contextCopyPath"; - _contextCopyPath.Size = new Size(225, 22); - _contextCopyPath.Text = "Copy Path"; - _contextCopyPath.Click += ContextCopyPath_Click; - - // contextAllSeparator - _contextAllSeparator.Name = "contextAllSeparator"; - _contextAllSeparator.Size = new Size(235, 6); - - // contextSelectAll - ContextSelectAll.Name = "contextSelectAll"; - ContextSelectAll.Size = new Size(225, 22); - ContextSelectAll.Text = "Select All"; - ContextSelectAll.Click += ContextSelectAll_Click; - - // contextDeselectAll - ContextDeselectAll.Name = "contextDeselectAll"; - ContextDeselectAll.Size = new Size(225, 22); - ContextDeselectAll.Text = "Deselect All"; - ContextDeselectAll.Click += ContextDeselectAll_Click; - - // contextExpandAll - _contextExpandAll.Name = "contextExpandAll"; - _contextExpandAll.Size = new Size(225, 22); - _contextExpandAll.Text = "Expand All"; - _contextExpandAll.Click += ContextExpandAll_Click; - - // contextCollapseAll - _contextCollapseAll.Name = "contextCollapseAll"; - _contextCollapseAll.Size = new Size(225, 22); - _contextCollapseAll.Text = "Collapse All"; - _contextCollapseAll.Click += ContextCollapseAll_Click; - } - - protected void BuildContextMenu() - { - _contextMenu.Items.Clear(); - _contextMenu.Items.AddRange(ContextOpenRegion.Items); - _contextMenu.Items.AddRange(ContextNodeRegion.Items); - _contextMenu.Items.AddRange(ContextAllRegion.Items); - } - - void ResetContextItemAvailability() - { - foreach (var menuItem in _contextMenu.Items.OfType()) - menuItem.Available = false; - } - - protected virtual void SetContextItemAvailability() - { - foreach (var menuItem in _contextMenu.Items.OfType()) - menuItem.Available = false; - - if (ClickedNode != null && ClickedNode.Tag is NodeMetadata) - { - _contextCopyPath.Available = true; - if (IsFileNode(ClickedNode) - && !((ModFileCategory)ClickedNode.Parent.Tag).IsBundled - && File.Exists((ClickedNode.Tag as NodeMetadata).ModFile.GetVanillaFile())) - { - _contextOpenVanillaFile.Available = true; - _contextOpenVanillaFileDir.Available = true; - } - else if (IsModNode(ClickedNode)) - { - if (ClickedNode.GetMetadata().ModFile.IsBundleContent) - _contextOpenModBundleDir.Available = true; - else - _contextOpenModFile.Available = _contextOpenModFileDir.Available = true; - } - } - - if (ClickedNode == null && !this.IsEmpty()) - { - _contextExpandAll.Available = - CategoryNodes.Any(catNode => !catNode.IsExpanded) - || FileNodes.Any(fileNode => !fileNode.IsExpanded); - - _contextCollapseAll.Available = CategoryNodes.Any(node => node.IsExpanded); - - _contextAllSeparator.Visible = - (_contextExpandAll.Available || _contextCollapseAll.Available) - && (ContextOpenRegion.Available || ContextNodeRegion.Available); - } - } - - void SetContextMenuSize() - { - if (_contextMenu.Items.OfType().Any(item => item.Available)) - { - var width = _contextMenu.Items.OfType().Where(item => item.Available) - .Max(item => TextRenderer.MeasureText(item.Text, item.Font).Width); - var height = _contextMenu.GetAvailableItems() - .Sum(item => item.Height); - _contextMenu.Width = width + 45; - _contextMenu.Height = height + 5; - } - } - - protected void ContextOpenFile_Click(object sender, EventArgs e) - { - if (RightClickedNode == null) - return; - - Program.TryOpenFile(RightClickedNode.GetMetadata().FilePath); - - RightClickedNode = null; - } - - protected void ContextOpenDirectory_Click(object sender, EventArgs e) - { - if (RightClickedNode == null) - return; - - Program.TryOpenFileLocation(RightClickedNode.GetMetadata().FilePath); - - RightClickedNode = null; - } - - protected void ContextOpenVanillaFile_Click(object sender, EventArgs e) - { - if (RightClickedNode == null) - return; - - Program.TryOpenFile(RightClickedNode.GetMetadata().ModFile.GetVanillaFile()); - - RightClickedNode = null; - } - - protected void ContextOpenVanillaDirectory_Click(object sender, EventArgs e) - { - if (RightClickedNode == null) - return; - - Program.TryOpenFileLocation(RightClickedNode.GetMetadata().ModFile.GetVanillaFile()); - - RightClickedNode = null; - } - - void ContextCopyPath_Click(object sender, EventArgs e) - { - if (RightClickedNode == null) - return; - - Clipboard.SetText(RightClickedNode.GetMetadata().FilePath); - - RightClickedNode = null; - } - - protected void ContextSelectAll_Click(object sender, EventArgs e) - { - SetAllChecked(true); - } - - protected void ContextDeselectAll_Click(object sender, EventArgs e) - { - SetAllChecked(false); - } - - protected abstract void SetAllChecked(bool isChecked); - - void ContextExpandAll_Click(object sender, EventArgs e) - { - ExpandAll(); - } - - void ContextCollapseAll_Click(object sender, EventArgs e) - { - CollapseAll(); - } - - void ContextMenu_Closing(object sender, ToolStripDropDownClosingEventArgs e) - { - if (ClickedNode == null) - return; - ClickedNode.BackColor = Color.Transparent; - ClickedNode.TreeView.Update(); - - RightClickedNode = ClickedNode; // Preserve reference to clicked node so context item handlers can access, - ClickedNode = null; // but clear ClickedNode so mouseover doesn't change back color. - } - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using WitcherScriptMerger.FileIndex; +using WitcherScriptMerger.Inventory; + +namespace WitcherScriptMerger.Controls +{ + abstract class SMTree : TreeView + { + #region Types + + public enum LevelType : int + { + Categories, Files, Mods + } + + public class NodeMetadata + { + public string FilePath; + public FileHash FileHash; + public ModFile ModFile; + } + + #endregion + + #region Members + + public static readonly Color FileNodeForeColor = Color.Black; + + public List CategoryNodes => GetNodesAtLevel(LevelType.Categories); + + public List FileNodes => GetNodesAtLevel(LevelType.Files); + + public List ModNodes => GetNodesAtLevel(LevelType.Mods); + + protected TreeNode ClickedNode = null; + protected bool IsUpdating = false; + + Color _clickedNodeForeColor; + + #endregion + + #region Double-buffering + + // From http://stackoverflow.com/a/10364283/1641069 + // Pinvoke: + private const int TVM_SETEXTENDEDSTYLE = 0x1100 + 44; + private const int TVS_EX_DOUBLEBUFFER = 0x0004; + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp); + + protected override void OnHandleCreated(EventArgs e) + { + SendMessage(this.Handle, TVM_SETEXTENDEDSTYLE, (IntPtr)TVS_EX_DOUBLEBUFFER, (IntPtr)TVS_EX_DOUBLEBUFFER); + base.OnHandleCreated(e); + } + + #endregion + + #region Context Menu Members + + protected TreeNode RightClickedNode; + + ContextMenuStrip _contextMenu; + + protected ToolStripRegion ContextOpenRegion; + ToolStripMenuItem _contextOpenModFile = new ToolStripMenuItem(); + ToolStripMenuItem _contextOpenModFileDir = new ToolStripMenuItem(); + ToolStripMenuItem _contextOpenModBundleDir = new ToolStripMenuItem(); + ToolStripMenuItem _contextOpenVanillaFile = new ToolStripMenuItem(); + ToolStripMenuItem _contextOpenVanillaFileDir = new ToolStripMenuItem(); + ToolStripMenuItem _contextCopyPath = new ToolStripMenuItem(); + + protected ToolStripRegion ContextNodeRegion; + + protected ToolStripRegion ContextAllRegion; + ToolStripSeparator _contextAllSeparator = new ToolStripSeparator(); + ToolStripMenuItem _contextExpandAll = new ToolStripMenuItem(); + ToolStripMenuItem _contextCollapseAll = new ToolStripMenuItem(); + protected ToolStripMenuItem ContextSelectAll = new ToolStripMenuItem(); + protected ToolStripMenuItem ContextDeselectAll = new ToolStripMenuItem(); + + #endregion + + public SMTree() + { + InitializeContextMenu(); + TreeViewNodeSorter = new SMTreeSorter(); + } + + protected List GetNodesAtLevel(LevelType level) + { + var nodes = Nodes.Cast(); + for (int i = 0; i < (int)level; ++i) + nodes = nodes.SelectMany(node => node.GetTreeNodes()); + return nodes.ToList(); + } + + public TreeNode GetCategoryNode(ModFileCategory category) + { + return CategoryNodes.FirstOrDefault(node => + category == (ModFileCategory)node.Tag); + } + + public void SetFontBold(LevelType level) + { + BeginUpdate(); + foreach (var node in GetNodesAtLevel(level)) + node.SetFontStyle(FontStyle.Bold); + EndUpdate(); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Control) + { + if (e.KeyCode == Keys.A) + ContextSelectAll_Click(null, null); + else if (e.KeyCode == Keys.D) + ContextDeselectAll_Click(null, null); + } + } + + protected override void OnAfterSelect(TreeViewEventArgs e) + { + base.OnAfterSelect(e); + SelectedNode = null; + } + + protected override void OnMouseDown(MouseEventArgs e) + { + base.OnMouseDown(e); + ClickedNode = GetNodeAt(e.Location); + if (ClickedNode != null) + { + if (!ClickedNode.Bounds.Contains(e.Location)) + ClickedNode = null; + else if (e.Button == MouseButtons.Left) + { + _clickedNodeForeColor = ClickedNode.ForeColor; + ClickedNode.ForeColor = Color.White; + ClickedNode.BackColor = Color.CornflowerBlue; + } + } + + if (e.Button == MouseButtons.Right) + { + BeginUpdate(); + IsUpdating = true; + } + } + + protected override void OnMouseMove(MouseEventArgs e) + { + base.OnMouseMove(e); + if (ClickedNode == null || RightClickedNode != null || e.Button == MouseButtons.Right) + return; + if (ClickedNode.Bounds.Contains(e.Location)) + { + ClickedNode.BackColor = Color.CornflowerBlue; + ClickedNode.ForeColor = Color.White; + } + else + { + ClickedNode.ForeColor = _clickedNodeForeColor; + ClickedNode.BackColor = Color.Transparent; + } + } + + protected override void OnMouseUp(MouseEventArgs e) + { + base.OnMouseUp(e); + + var lastClicked = ClickedNode; + ClickedNode = GetNodeAt(e.Location); + if (ClickedNode != null && + (lastClicked != ClickedNode || !ClickedNode.Bounds.Contains(e.Location))) + ClickedNode = null; + + if (e.Button == MouseButtons.Left) + { + OnLeftMouseUp(e); + if (lastClicked != null && ClickedNode != null) + { + lastClicked.ForeColor = _clickedNodeForeColor; + lastClicked.BackColor = Color.Transparent; + } + ClickedNode = null; + } + else if (e.Button == MouseButtons.Right) + OnRightMouseUp(e); + EndUpdate(); + IsUpdating = false; + } + + protected virtual void OnLeftMouseUp(MouseEventArgs e) + { + if (ClickedNode == null) + return; + if (ClickedNode.SetCheckedIfVisible(!ClickedNode.Checked)) + HandleCheckedChange(); + } + + protected virtual void OnRightMouseUp(MouseEventArgs e) + { + ResetContextItemAvailability(); + SetContextItemAvailability(); + + if (_contextMenu.Items.OfType().Any(item => item.Available)) + { + if (ClickedNode != null) + ClickedNode.BackColor = Color.Gainsboro; + + SetContextMenuSize(); + + _contextMenu.Show(this, e.X, e.Y); + } + } + + protected override void OnAfterCheck(TreeViewEventArgs e) + { + base.OnAfterCheck(e); + if (e.Action != TreeViewAction.Unknown) // Event was triggered programmatically + { + ClickedNode = e.Node; + HandleCheckedChange(); + } + } + + protected abstract void HandleCheckedChange(); + + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + + if (IsUpdating) + EndUpdate(); + } + + protected bool IsCategoryNode(TreeNode node) => ((LevelType)node.Level == LevelType.Categories); + + protected bool IsFileNode(TreeNode node) => ((LevelType)node.Level == LevelType.Files); + + protected bool IsModNode(TreeNode node) => ((LevelType)node.Level == LevelType.Mods); + + #region Context Menu + + void InitializeContextMenu() + { + _contextMenu = new ContextMenuStrip(); + + ContextOpenRegion = new ToolStripRegion(_contextMenu as ToolStrip, new ToolStripItem[] + { + _contextCopyPath, + _contextOpenModFile, + _contextOpenModFileDir, + _contextOpenModBundleDir, + _contextOpenVanillaFile, + _contextOpenVanillaFileDir + }); + + ContextNodeRegion = new ToolStripRegion(_contextMenu as ToolStrip, new ToolStripItem[0]); + + ContextAllRegion = new ToolStripRegion(_contextMenu as ToolStrip, new ToolStripItem[] + { + _contextAllSeparator, + ContextSelectAll, + ContextDeselectAll, + _contextExpandAll, + _contextCollapseAll + }); + + // treeContextMenu + _contextMenu.AutoSize = false; + _contextMenu.Name = "treeContextMenu"; + _contextMenu.Size = new Size(239, 390); + _contextMenu.Closing += ContextMenu_Closing; + + // contextOpenModFile + _contextOpenModFile.Name = "contextOpenModFile"; + _contextOpenModFile.Size = new Size(225, 22); + _contextOpenModFile.Text = "Open Mod File"; + _contextOpenModFile.ToolTipText = "Opens this mod's version of the file"; + _contextOpenModFile.Click += ContextOpenFile_Click; + + // contextOpenModFileDir + _contextOpenModFileDir.Name = "contextOpenModFileDir"; + _contextOpenModFileDir.Size = new Size(225, 22); + _contextOpenModFileDir.Text = "Open Mod File Directory"; + _contextOpenModFileDir.ToolTipText = "Opens the location of this mod's version of the file"; + _contextOpenModFileDir.Click += ContextOpenDirectory_Click; + + // contextOpenModBundleDir + _contextOpenModBundleDir.Name = "contextOpenModBundleDir"; + _contextOpenModBundleDir.Size = new Size(225, 22); + _contextOpenModBundleDir.Text = "Open Mod Bundle Directory"; + _contextOpenModBundleDir.ToolTipText = "Opens the location of this mod's bundle file"; + _contextOpenModBundleDir.Click += ContextOpenDirectory_Click; + + // contextOpenVanillaFile + _contextOpenVanillaFile.Name = "contextOpenVanillaFile"; + _contextOpenVanillaFile.Size = new Size(225, 22); + _contextOpenVanillaFile.Text = "Open Vanilla File"; + _contextOpenVanillaFile.ToolTipText = "Opens the unmodded version of the file"; + _contextOpenVanillaFile.Click += ContextOpenVanillaFile_Click; + + // contextOpenVanillaFileDir + _contextOpenVanillaFileDir.Name = "contextOpenVanillaFileDir"; + _contextOpenVanillaFileDir.Size = new Size(225, 22); + _contextOpenVanillaFileDir.Text = "Open Vanilla File Directory"; + _contextOpenVanillaFileDir.ToolTipText = "Opens the location of the unmodded version of the file"; + _contextOpenVanillaFileDir.Click += ContextOpenVanillaDirectory_Click; + + // contextCopyPath + _contextCopyPath.Name = "contextCopyPath"; + _contextCopyPath.Size = new Size(225, 22); + _contextCopyPath.Text = "Copy Path"; + _contextCopyPath.Click += ContextCopyPath_Click; + + // contextAllSeparator + _contextAllSeparator.Name = "contextAllSeparator"; + _contextAllSeparator.Size = new Size(235, 6); + + // contextSelectAll + ContextSelectAll.Name = "contextSelectAll"; + ContextSelectAll.Size = new Size(225, 22); + ContextSelectAll.Text = "Select All"; + ContextSelectAll.Click += ContextSelectAll_Click; + + // contextDeselectAll + ContextDeselectAll.Name = "contextDeselectAll"; + ContextDeselectAll.Size = new Size(225, 22); + ContextDeselectAll.Text = "Deselect All"; + ContextDeselectAll.Click += ContextDeselectAll_Click; + + // contextExpandAll + _contextExpandAll.Name = "contextExpandAll"; + _contextExpandAll.Size = new Size(225, 22); + _contextExpandAll.Text = "Expand All"; + _contextExpandAll.Click += ContextExpandAll_Click; + + // contextCollapseAll + _contextCollapseAll.Name = "contextCollapseAll"; + _contextCollapseAll.Size = new Size(225, 22); + _contextCollapseAll.Text = "Collapse All"; + _contextCollapseAll.Click += ContextCollapseAll_Click; + } + + protected void BuildContextMenu() + { + _contextMenu.Items.Clear(); + _contextMenu.Items.AddRange(ContextOpenRegion.Items); + _contextMenu.Items.AddRange(ContextNodeRegion.Items); + _contextMenu.Items.AddRange(ContextAllRegion.Items); + } + + void ResetContextItemAvailability() + { + foreach (var menuItem in _contextMenu.Items.OfType()) + menuItem.Available = false; + } + + protected virtual void SetContextItemAvailability() + { + foreach (var menuItem in _contextMenu.Items.OfType()) + menuItem.Available = false; + + if (ClickedNode != null && ClickedNode.Tag is NodeMetadata) + { + _contextCopyPath.Available = true; + if (IsFileNode(ClickedNode) + && !((ModFileCategory)ClickedNode.Parent.Tag).IsBundled + && File.Exists((ClickedNode.Tag as NodeMetadata).ModFile.GetVanillaFile())) + { + _contextOpenVanillaFile.Available = true; + _contextOpenVanillaFileDir.Available = true; + } + else if (IsModNode(ClickedNode)) + { + if (ClickedNode.GetMetadata().ModFile.IsBundleContent) + _contextOpenModBundleDir.Available = true; + else + _contextOpenModFile.Available = _contextOpenModFileDir.Available = true; + } + } + + if (ClickedNode == null && !this.IsEmpty()) + { + _contextExpandAll.Available = + CategoryNodes.Any(catNode => !catNode.IsExpanded) + || FileNodes.Any(fileNode => !fileNode.IsExpanded); + + _contextCollapseAll.Available = CategoryNodes.Any(node => node.IsExpanded); + + _contextAllSeparator.Visible = + (_contextExpandAll.Available || _contextCollapseAll.Available) + && (ContextOpenRegion.Available || ContextNodeRegion.Available); + } + } + + void SetContextMenuSize() + { + if (_contextMenu.Items.OfType().Any(item => item.Available)) + { + var width = _contextMenu.Items.OfType().Where(item => item.Available) + .Max(item => TextRenderer.MeasureText(item.Text, item.Font).Width); + var height = _contextMenu.GetAvailableItems() + .Sum(item => item.Height); + _contextMenu.Width = width + 45; + _contextMenu.Height = height + 5; + } + } + + protected void ContextOpenFile_Click(object sender, EventArgs e) + { + if (RightClickedNode == null) + return; + + Program.TryOpenFile(RightClickedNode.GetMetadata().FilePath); + + RightClickedNode = null; + } + + protected void ContextOpenDirectory_Click(object sender, EventArgs e) + { + if (RightClickedNode == null) + return; + + Program.TryOpenFileLocation(RightClickedNode.GetMetadata().FilePath); + + RightClickedNode = null; + } + + protected void ContextOpenVanillaFile_Click(object sender, EventArgs e) + { + if (RightClickedNode == null) + return; + + Program.TryOpenFile(RightClickedNode.GetMetadata().ModFile.GetVanillaFile()); + + RightClickedNode = null; + } + + protected void ContextOpenVanillaDirectory_Click(object sender, EventArgs e) + { + if (RightClickedNode == null) + return; + + Program.TryOpenFileLocation(RightClickedNode.GetMetadata().ModFile.GetVanillaFile()); + + RightClickedNode = null; + } + + void ContextCopyPath_Click(object sender, EventArgs e) + { + if (RightClickedNode == null) + return; + + Clipboard.SetText(RightClickedNode.GetMetadata().FilePath); + + RightClickedNode = null; + } + + protected void ContextSelectAll_Click(object sender, EventArgs e) + { + SetAllChecked(true); + } + + protected void ContextDeselectAll_Click(object sender, EventArgs e) + { + SetAllChecked(false); + } + + protected abstract void SetAllChecked(bool isChecked); + + void ContextExpandAll_Click(object sender, EventArgs e) + { + ExpandAll(); + } + + void ContextCollapseAll_Click(object sender, EventArgs e) + { + CollapseAll(); + } + + void ContextMenu_Closing(object sender, ToolStripDropDownClosingEventArgs e) + { + if (ClickedNode == null) + return; + ClickedNode.BackColor = Color.Transparent; + ClickedNode.TreeView.Update(); + + RightClickedNode = ClickedNode; // Preserve reference to clicked node so context item handlers can access, + ClickedNode = null; // but clear ClickedNode so mouseover doesn't change back color. + } + + #endregion + } +} diff --git a/WitcherScriptMerger/Forms/MessageBoxManager.cs b/WitcherScriptMerger/Forms/MessageBoxManager.cs index 2ec8d5f..7b5d739 100644 --- a/WitcherScriptMerger/Forms/MessageBoxManager.cs +++ b/WitcherScriptMerger/Forms/MessageBoxManager.cs @@ -11,12 +11,11 @@ class MessageBoxManager private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); private delegate bool EnumChildProc(IntPtr hWnd, IntPtr lParam); + // Only the Win32 constants this class actually reads - the vendored original + // carried the fuller WM_* set (WM_DESTROY/WM_TIMER/WM_USER/DM_GETDEFID), unused + // here since this class only ever reacts to WM_INITDIALOG. private const int WH_CALLWNDPROCRET = 12; - private const int WM_DESTROY = 0x0002; private const int WM_INITDIALOG = 0x0110; - private const int WM_TIMER = 0x0113; - private const int WM_USER = 0x400; - private const int DM_GETDEFID = WM_USER + 0; private const int MBOK = 1; private const int MBCancel = 2; diff --git a/WitcherScriptMerger/Program.cs b/WitcherScriptMerger/Program.cs index 04e3d95..e8e9d11 100644 --- a/WitcherScriptMerger/Program.cs +++ b/WitcherScriptMerger/Program.cs @@ -20,22 +20,25 @@ namespace WitcherScriptMerger { static class Program { - // Must run before anything else in this class's static init, so any - // early startup error (e.g. missing App.config) is visible in the - // invoking terminal instead of written to an unattached console. - static readonly bool _consoleAttached = MaybeAttachConsole(); - - // Explicit (empty) static constructor: without one, the C# compiler marks - // this class `beforefieldinit`, under which the CLR is free to defer running - // _consoleAttached's initializer until Program's own static fields are first - // touched - Main() itself no longer counts, since Notifier/Settings/LoadOrder/ - // Inventory became pass-through properties to AppState (see below) rather than - // fields, so nothing in Main() necessarily touches a field of this class at - // all. Confirmed empirically with a minimal repro mirroring this exact shape: - // without this constructor, the field initializer's side effect (here, - // MaybeAttachConsole()) never ran at all during a normal Main() invocation. - // Do not remove this without re-verifying that repro. - static Program() { } + // Explicit static constructor, calling MaybeAttachConsole() directly: it must + // run before anything else in this class, so any early startup error (e.g. + // missing App.config) is visible in the invoking terminal instead of written + // to an unattached console. Two deliberate choices here, both load-bearing: + // the explicit constructor (without one, the compiler marks this class + // `beforefieldinit`, under which the CLR is free to defer static init - and + // Main() no longer necessarily touches a field of this class at all, since + // Notifier/Settings/LoadOrder/Inventory became pass-through properties to + // AppState below; confirmed empirically with a minimal repro that a field + // initializer's side effect never ran during a normal Main() invocation + // without it - do not remove without re-verifying that repro), and the call + // living IN the constructor body rather than a `static readonly bool + // _consoleAttached = ...` field initializer (the previous shape - the field's + // value was never read, only its initializer's side effect mattered, which + // both tripped CA1823 and obscured that the side effect is the whole point). + static Program() + { + MaybeAttachConsole(); + } // Notifier/Settings/LoadOrder/Inventory live in Core's AppState now, not here - // domain code that moved to Core (Paths, FileMerger, Cli/MergeOperations, diff --git a/vortex-extension/src/githubRelease.ts b/vortex-extension/src/githubRelease.ts index 7d4421f..79b47e8 100644 --- a/vortex-extension/src/githubRelease.ts +++ b/vortex-extension/src/githubRelease.ts @@ -3,12 +3,11 @@ import * as https from 'https'; /** * Download-from-GitHub-Releases logic for acquiring a WSM build. Parameterized on - * `repo`/`tag` (never hardcoded beyond `DEFAULT_WSM_REPO`'s default) so it's genuinely - * functional once a release actually exists - **as of this unit, no version tag has - * been pushed to this repo, so no GitHub Release exists yet and this code path has - * never been exercised against a real release; see this unit's PR description.** - * Verified here only via `githubRelease.test.ts`'s mocked `HttpClient` - no real network - * call is made by any test. + * `repo`/`tag` (never hardcoded beyond `DEFAULT_WSM_REPO`'s default). A real release + * now exists (v0.6.2, published 2026-08-11, with exactly the asset names this module + * expects), so this path is exercisable for real - unit tests still go through + * `githubRelease.test.ts`'s mocked `HttpClient` only (no test makes a real network + * call). * * Asset naming matches `.github/workflows/release.yml`'s `package-release` job exactly: * `WitcherScriptMerger.Headless--win-x64.zip` (the CLI/MCP-only, no-GUI host - diff --git a/vortex-extension/src/index.ts b/vortex-extension/src/index.ts index 57dc794..9f728a4 100644 --- a/vortex-extension/src/index.ts +++ b/vortex-extension/src/index.ts @@ -15,13 +15,12 @@ import { ensureWsmToolRegistered } from './toolAcquisition'; * This unit (tool acquisition) is the first to add real registration: re-registering a * previously-acquired WSM binary as a discovered tool, via `ensureWsmToolRegistered` * (`./toolAcquisition`) - a **local-only, network-free** check, safe to run - * unconditionally on every load. Deliberately not an eager background *download* here: - * as of this unit, no GitHub Release exists on this repo yet (see - * `githubRelease.ts`'s own doc comment), so attempting one on every Vortex startup - * would just be a guaranteed, noisy failure with nothing to show for it. - * `./toolAcquisition`'s `acquireWsmTool` (the actual download/verify/extract/register - * pipeline) is exported for a later unit's own UI trigger (a "Get WitcherScriptMerger" - * action, once one exists) to call on explicit user request instead. + * unconditionally on every load. Deliberately not an eager background *download* here, + * even now that a real GitHub Release exists (v0.6.2): downloading is a network side + * effect a user should trigger explicitly, not something every Vortex startup performs + * unprompted. `./toolAcquisition`'s `acquireWsmTool` (the actual + * download/verify/extract/register pipeline) is exported for that explicit UI trigger + * (a "Get WitcherScriptMerger" action) to call on user request instead. * * Re-evaluated on every `'gamemode-activated'` event (`@nexusmods/vortex-api`'s own * README documents this event, firing with the newly-active game's id), not just once diff --git a/vortex-extension/src/toolAcquisition.ts b/vortex-extension/src/toolAcquisition.ts index 81740f9..a80c46e 100644 --- a/vortex-extension/src/toolAcquisition.ts +++ b/vortex-extension/src/toolAcquisition.ts @@ -11,10 +11,11 @@ import { buildWsmEnv } from './wsmEnv'; /** * Orchestrates the full acquisition pipeline (download from GitHub Releases -> verify -> * extract -> register as a discovered tool) and a lighter local-only re-registration - * path used at every Vortex startup. See this unit's PR description for exactly what's - * verified end-to-end (a locally-built WSM binary standing in for a downloaded one, per - * `test/toolAcquisition.integration.test.ts`) versus what's real-but-unexercised code - * (the actual GitHub download - no release exists on this repo yet). + * path used at every Vortex startup. The end-to-end verified part is the + * extract/register pipeline (a locally-built WSM binary standing in for a downloaded + * one, per `test/toolAcquisition.integration.test.ts`); the actual GitHub download runs + * against the real v0.6.2 release (which exists now, with matching asset names) only + * outside the test suite - no test makes a real network call. */ export const WSM_HEADLESS_EXE_NAME = 'WitcherScriptMerger.Headless.exe'; @@ -220,8 +221,8 @@ function registerAcquiredTool(api: types.IExtensionApi, exePath: string): void { * re-registering on every startup is the safe, idempotent default rather than assuming * a prior registration survived. * - * Returns `false` (not an error) when nothing has been acquired yet - that's the - * expected, normal state for as long as no GitHub Release exists. + * Returns `false` (not an error) when nothing has been acquired yet - the expected, + * normal state on a fresh install until the user triggers an acquisition. */ export async function ensureWsmToolRegistered(api: types.IExtensionApi): Promise { const installDir = getWsmToolDir(api);