From a1307c6eff6645a15192ab94060f22f3480a8f9c Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:16:32 +0100 Subject: [PATCH 01/13] Added warning for unsupported node event types --- TombEditor/Controls/TriggerManager.cs | 7 ++++ TombEditor/Forms/FormEventSetEditor.cs | 2 + .../Controls/VisualScripting/NodeEditor.cs | 40 +++++++++++++++++-- .../TriggerNodeEnumerations.cs | 14 +++++++ TombLib/TombLib/Utils/ScriptingUtils.cs | 23 +++++++++++ 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/TombEditor/Controls/TriggerManager.cs b/TombEditor/Controls/TriggerManager.cs index 4e8b890ffd..a04053a059 100644 --- a/TombEditor/Controls/TriggerManager.cs +++ b/TombEditor/Controls/TriggerManager.cs @@ -24,6 +24,13 @@ public TriggerManager() InitializeComponent(); } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EventType? EventType + { + get { return nodeEditor.CurrentEventType; } + set { nodeEditor.CurrentEventType = value; } + } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Event Event { diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index 6b6b37ae76..6284e71754 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -429,6 +429,7 @@ private void LoadEventSetIntoUI(EventSet newEventSet) } cbEvents.SelectedItem = newEventSet.LastUsedEvent; + triggerManager.EventType = newEventSet.LastUsedEvent; triggerManager.Event = newEventSet.Events[newEventSet.LastUsedEvent]; tbName.Text = newEventSet.Name; @@ -646,6 +647,7 @@ private void cbEvents_SelectedIndexChanged(object sender, EventArgs e) if (!_lockUI) { SelectedSet.LastUsedEvent = (EventType)cbEvents.SelectedItem; + triggerManager.EventType = SelectedSet.LastUsedEvent; triggerManager.Event = SelectedSet.Events[SelectedSet.LastUsedEvent]; } } diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index da24befb8a..8867957991 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -73,6 +73,10 @@ public List Nodes [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public List NodeFunctions { get; private set; } = new List(); + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EventType? CurrentEventType { get; set; } + // Precache lists of objects to avoid polling every time user changes function // in a node list. By default it is set to nothing, but must be replaced externally // by a form/control where node editor is placed. @@ -885,20 +889,50 @@ private void DrawShadow(PaintEventArgs e, VisibleNodeBase node) e.Graphics.DrawImage(Properties.Resources.misc_Shadow, rect); } + private bool IsNodeUnsupported(TriggerNode node) + { + if (!CurrentEventType.HasValue || string.IsNullOrEmpty(node.Function)) + return false; + + var func = NodeFunctions.FirstOrDefault(f => f.Signature == node.Function); + if (func == null) + return false; + + return func.IsUnsupported(CurrentEventType.Value); + } + private void DrawHeader(PaintEventArgs e, VisibleNodeBase node) { if (!node.Visible) return; - var size = TextRenderer.MeasureText(node.Node.Name, Font); + const float LabelOpacity = 0.5f; + + bool unsupported = IsNodeUnsupported(node.Node); + var headerText = unsupported ? "Not supported for this event type" : node.Node.Name; + int iconOffset = 0; + var size = TextRenderer.MeasureText(headerText, Font); var rect = node.ClientRectangle; rect.Height = size.Height; rect.Offset(node.Location); rect.Offset(0, -(int)(size.Height * 1.2f)); - using (var b = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(0.5f))) - e.Graphics.DrawString(node.Node.Name, Font, b, rect, + if (unsupported) + { + var matrix = new System.Drawing.Imaging.ColorMatrix { Matrix33 = LabelOpacity, Matrix22 = 0.0f }; + var attributes = new System.Drawing.Imaging.ImageAttributes(); + attributes.SetColorMatrix(matrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); + + var icon = Properties.Resources.general_Warning_16; + var iconRect = new Rectangle(rect.X, rect.Y + (rect.Height - icon.Height), icon.Width, icon.Height); + e.Graphics.DrawImage(icon, iconRect, 0, 0, icon.Width, icon.Height, GraphicsUnit.Pixel, attributes); + iconOffset = icon.Width + 2; + } + + var textRect = new Rectangle(rect.X + iconOffset, rect.Y, rect.Width - iconOffset, rect.Height); + using (var b = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(LabelOpacity))) + e.Graphics.DrawString(headerText, Font, b, textRect, new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center }); var condNode = node as VisibleNodeCondition; diff --git a/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs b/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs index 08e1e09650..4e0da52f29 100644 --- a/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs +++ b/TombLib/TombLib/LevelData/VisualScripting/TriggerNodeEnumerations.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using TombLib.LevelData; namespace TombLib.LevelData.VisualScripting { @@ -78,6 +79,19 @@ public class NodeFunction public bool Conditional { get; set; } public string Signature { get; set; } public List Arguments { get; private set; } = new List(); + public List SupportedEvents { get; private set; } = new List(); + public List UnsupportedEvents { get; private set; } = new List(); + + public bool IsUnsupported(EventType eventType) + { + if (SupportedEvents.Count > 0 && !SupportedEvents.Contains(eventType)) + return true; + + if (UnsupportedEvents.Count > 0 && UnsupportedEvents.Contains(eventType)) + return true; + + return false; + } public override string ToString() => Name; public override int GetHashCode() => (Name + Conditional.ToString() + Description + Signature + Arguments.Count.ToString()).GetHashCode(); diff --git a/TombLib/TombLib/Utils/ScriptingUtils.cs b/TombLib/TombLib/Utils/ScriptingUtils.cs index e0b0daf77f..adad2e4a98 100644 --- a/TombLib/TombLib/Utils/ScriptingUtils.cs +++ b/TombLib/TombLib/Utils/ScriptingUtils.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using TombLib.LevelData; using TombLib.LevelData.VisualScripting; namespace TombLib.Utils @@ -59,6 +60,8 @@ public static class ScriptingUtils private const string _nodeTypeId = _metadataPrefix + "condition"; private const string _nodeArgumentId = _metadataPrefix + "arguments"; private const string _nodeDescriptionId = _metadataPrefix + "description"; + private const string _nodeSupportedId = _metadataPrefix + "supported"; + private const string _nodeUnsupportedId = _metadataPrefix + "unsupported"; private const string _nodeLayoutNewLine = "newline"; public static string GameNodeScriptPath = Path.Combine("Scripts", "Engine", "NodeCatalogs"); @@ -156,6 +159,16 @@ private static List GetAllNodeFunctions(string path, List 0) @@ -257,6 +270,16 @@ private static List GetAllNodeFunctions(string path, List n.Section).ToList(); } + private static void ParseEventTypeList(string comment, string tagId, List targetList) + { + var values = TextExtensions.ExtractValues(comment.Substring(tagId.Length, comment.Length - tagId.Length)); + foreach (var v in values) + { + if (Enum.TryParse(v.Trim(), out EventType eventType)) + targetList.Add(eventType); + } + } + public static List GetAllFunctionNames(string path, List list = null, int depth = 0) { var result = list == null ? new List() : list; From 1828cd57dead2424547272b18bf08337531006c4 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:17:44 +0100 Subject: [PATCH 02/13] Use non-nullable type --- TombEditor/Controls/TriggerManager.cs | 2 +- TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TombEditor/Controls/TriggerManager.cs b/TombEditor/Controls/TriggerManager.cs index a04053a059..30b5847f74 100644 --- a/TombEditor/Controls/TriggerManager.cs +++ b/TombEditor/Controls/TriggerManager.cs @@ -25,7 +25,7 @@ public TriggerManager() } [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public EventType? EventType + public EventType EventType { get { return nodeEditor.CurrentEventType; } set { nodeEditor.CurrentEventType = value; } diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index 8867957991..4b75db4df9 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -75,7 +75,7 @@ public List Nodes [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public EventType? CurrentEventType { get; set; } + public EventType CurrentEventType { get; set; } // Precache lists of objects to avoid polling every time user changes function // in a node list. By default it is set to nothing, but must be replaced externally From 22a81adec41e0a9a4bf1f6a78f03bd7af9a3a24a Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 15 Mar 2026 09:59:10 +0100 Subject: [PATCH 03/13] Update NodeEditor.cs --- .../Controls/VisualScripting/NodeEditor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index 4b75db4df9..2e3ce481f6 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -25,6 +25,10 @@ public enum ConnectionMode public partial class NodeEditor : UserControl { + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public EventType CurrentEventType { get; set; } + [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Vector2 ViewPosition { get; set; } = new Vector2(60.0f, 60.0f); @@ -73,10 +77,6 @@ public List Nodes [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public List NodeFunctions { get; private set; } = new List(); - [Browsable(false)] - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - public EventType CurrentEventType { get; set; } - // Precache lists of objects to avoid polling every time user changes function // in a node list. By default it is set to nothing, but must be replaced externally // by a form/control where node editor is placed. @@ -891,14 +891,14 @@ private void DrawShadow(PaintEventArgs e, VisibleNodeBase node) private bool IsNodeUnsupported(TriggerNode node) { - if (!CurrentEventType.HasValue || string.IsNullOrEmpty(node.Function)) + if (string.IsNullOrEmpty(node.Function)) return false; var func = NodeFunctions.FirstOrDefault(f => f.Signature == node.Function); if (func == null) return false; - return func.IsUnsupported(CurrentEventType.Value); + return func.IsUnsupported(CurrentEventType); } private void DrawHeader(PaintEventArgs e, VisibleNodeBase node) From 2388d60635c3f2d6589067a93ecc3e175866ff00 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:29:42 +0100 Subject: [PATCH 04/13] Update Readme.md --- TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md index a20dcacdf6..b2b8b3db8d 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md @@ -40,6 +40,14 @@ Comment metadata entry reference (metadata block is indicated by a keyword which new line. - **!Section "SECTION"** - this will define where the node will be found inside Tomb Editor. + + - **!Supported "TYPE" "TYPE" "..." - defines supported event types for a given node. Each event type should be + enclosed in quotes. If event is not supported by a node, it will display a warning message when misplaced. + Possible event types are: **OnVolumeEnter, OnVolumeInside, OnVolumeLeave, OnLoop, OnLoadGame, OnSaveGame, + OnLevelStart, OnLevelEnd, OnUseItem, OnFreeze**. + + - **!Unsupported "TYPE" "TYPE" "..." - defines unsupported event types for a given node. Acts in an opposite way + to `!Supported`. Possible event types are equal to `!Supported`. - **!Arguments "ARGDESC1" "ARGDESC2" "ARGDESC..."** - infinite amount of args, with **ARGDESC** parameters separated by commas as follows: From 55e6f27208982431ff3c74ca071d462c495e67ef Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 15 Mar 2026 11:59:52 +0100 Subject: [PATCH 05/13] WIP --- DarkUI/DarkUI/Controls/DarkTreeView.cs | 58 ++- .../Forms/FormEventSetEditor.Designer.cs | 60 +-- TombEditor/Forms/FormEventSetEditor.cs | 432 ++++++++++++++---- TombLib/TombLib/LevelData/EventSet.cs | 1 + TombLib/TombLib/LevelData/IO/Prj2Chunks.cs | 1 + TombLib/TombLib/LevelData/IO/Prj2Loader.cs | 2 + TombLib/TombLib/LevelData/IO/Prj2Writer.cs | 2 + 7 files changed, 426 insertions(+), 130 deletions(-) diff --git a/DarkUI/DarkUI/Controls/DarkTreeView.cs b/DarkUI/DarkUI/Controls/DarkTreeView.cs index af11883542..02d2c5c436 100644 --- a/DarkUI/DarkUI/Controls/DarkTreeView.cs +++ b/DarkUI/DarkUI/Controls/DarkTreeView.cs @@ -22,6 +22,7 @@ public class DarkTreeView : DarkScrollView public event EventHandler SelectedNodesChanged; public event EventHandler AfterNodeExpand; public event EventHandler AfterNodeCollapse; + public event EventHandler NodesMoved; #endregion @@ -50,6 +51,7 @@ public class DarkTreeView : DarkScrollView private DarkTreeNode _provisionalNode; private DarkTreeNode _dropNode; private bool _provisionalDragging; + private bool _mouseInClientArea; private List _dragNodes; private Point _dragPos; @@ -154,6 +156,12 @@ public int Indent [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public IComparer TreeViewNodeSorter { get; set; } + // Optional predicate to restrict which nodes can act as drop targets. + // When null, any node may receive drops. + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public Func CanDropIntoNode { get; set; } + #endregion #region Constructor Region @@ -182,6 +190,7 @@ protected override void Dispose(bool disposing) SelectedNodesChanged = null; AfterNodeExpand = null; AfterNodeCollapse = null; + NodesMoved = null; _nodes?.Dispose(); @@ -644,7 +653,9 @@ private void DisposeIcons() private void CheckHover() { - if (!ClientRectangle.Contains(PointToClient(MousePosition))) + _mouseInClientArea = ClientRectangle.Contains(PointToClient(MousePosition)); + + if (!_mouseInClientArea) { if (IsDragging && _dropNode != null) { @@ -1091,8 +1102,17 @@ private void HandleDrag() if (dropNode == null) { - if (Cursor != Cursors.No) - Cursor = Cursors.No; + // Allow root-level drop when mouse is inside the control. + if (_mouseInClientArea) + { + if (Cursor != Cursors.SizeAll) + Cursor = Cursors.SizeAll; + } + else + { + if (Cursor != Cursors.No) + Cursor = Cursors.No; + } return; } @@ -1118,7 +1138,29 @@ private void HandleDrop() if (dropNode == null) { + // Drop to root when mouse is inside the control but not over any node. + if (_mouseInClientArea) + { + var cachedSelectedNodes = SelectedNodes.ToList(); + + foreach (var node in _dragNodes) + { + if (node.ParentNode == null) + Nodes.Remove(node); + else + node.ParentNode.Nodes.Remove(node); + + Nodes.Add(node); + } + + foreach (var node in cachedSelectedNodes) + SelectedNodes.Add(node); + + NodesMoved?.Invoke(this, EventArgs.Empty); + } + StopDrag(); + UpdateNodes(); return; } @@ -1143,6 +1185,8 @@ private void HandleDrop() foreach (var node in cachedSelectedNodes) SelectedNodes.Add(node); + + NodesMoved?.Invoke(this, EventArgs.Empty); } StopDrag(); @@ -1166,6 +1210,14 @@ private bool CanMoveNodes(IEnumerable dragNodes, DarkTreeNode drop if (dropNode == null) return false; + if (CanDropIntoNode != null && !CanDropIntoNode(dropNode)) + { + if (isMoving) + DarkMessageBox.Show(this, $"Cannot move nodes into '{dropNode.Text}'. This node cannot contain other nodes.", Application.ProductName, MessageBoxIcon.Error); + + return false; + } + foreach (var node in dragNodes) { if (node == dropNode) diff --git a/TombEditor/Forms/FormEventSetEditor.Designer.cs b/TombEditor/Forms/FormEventSetEditor.Designer.cs index 240049a1a0..cabeb370bf 100644 --- a/TombEditor/Forms/FormEventSetEditor.Designer.cs +++ b/TombEditor/Forms/FormEventSetEditor.Designer.cs @@ -27,12 +27,13 @@ private void InitializeComponent() cbActivatorStatics = new DarkCheckBox(); cbActivatorFlyBy = new DarkCheckBox(); panelList = new DarkSectionPanel(); - dgvEvents = new DarkDataGridView(); + treeEvents = new DarkTreeView(); darkPanel1 = new DarkPanel(); butSearch = new DarkButton(); butUnassignEventSet = new DarkButton(); butDeleteEventSet = new DarkButton(); butCloneEventSet = new DarkButton(); + butNewFolder = new DarkButton(); butNewEventSet = new DarkButton(); triggerManager = new Controls.TriggerManager(); lblActivators = new DarkLabel(); @@ -47,7 +48,6 @@ private void InitializeComponent() splitContainer = new SplitContainer(); panelActivators = new DarkSectionPanel(); panelList.SuspendLayout(); - ((System.ComponentModel.ISupportInitialize)dgvEvents).BeginInit(); darkPanel1.SuspendLayout(); panelEditor.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)splitContainer).BeginInit(); @@ -139,7 +139,7 @@ private void InitializeComponent() // panelList // panelList.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - panelList.Controls.Add(dgvEvents); + panelList.Controls.Add(treeEvents); panelList.Controls.Add(darkPanel1); panelList.Location = new System.Drawing.Point(3, 3); panelList.Name = "panelList"; @@ -147,27 +147,17 @@ private void InitializeComponent() panelList.Size = new System.Drawing.Size(258, 377); panelList.TabIndex = 22; // - // dgvEvents - // - dgvEvents.AllowUserToAddRows = false; - dgvEvents.AllowUserToDeleteRows = false; - dgvEvents.AllowUserToPasteCells = false; - dgvEvents.AllowUserToResizeColumns = false; - dgvEvents.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - dgvEvents.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; - dgvEvents.ColumnHeadersHeight = 4; - dgvEvents.ForegroundColor = System.Drawing.Color.FromArgb(220, 220, 220); - dgvEvents.Location = new System.Drawing.Point(4, 32); - dgvEvents.MultiSelect = false; - dgvEvents.Name = "dgvEvents"; - dgvEvents.ReadOnly = true; - dgvEvents.RowHeadersWidth = 41; - dgvEvents.Size = new System.Drawing.Size(250, 341); - dgvEvents.TabIndex = 0; - dgvEvents.UseAlternativeDragDropMethod = true; - dgvEvents.ColumnHeaderMouseClick += dgvEvents_ColumnHeaderMouseClick; - dgvEvents.SelectionChanged += dgvEvents_SelectedIndicesChanged; - dgvEvents.DragDrop += dgvEvents_DragDrop; + // treeEvents + // + treeEvents.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + treeEvents.Location = new System.Drawing.Point(4, 32); + treeEvents.MultiSelect = false; + treeEvents.Name = "treeEvents"; + treeEvents.AllowMoveNodes = true; + treeEvents.Size = new System.Drawing.Size(250, 341); + treeEvents.TabIndex = 0; + treeEvents.SelectedNodesChanged += treeEvents_SelectedNodesChanged; + treeEvents.DoubleClick += treeEvents_DoubleClick; // // darkPanel1 // @@ -175,6 +165,7 @@ private void InitializeComponent() darkPanel1.Controls.Add(butUnassignEventSet); darkPanel1.Controls.Add(butDeleteEventSet); darkPanel1.Controls.Add(butCloneEventSet); + darkPanel1.Controls.Add(butNewFolder); darkPanel1.Controls.Add(butNewEventSet); darkPanel1.Dock = DockStyle.Top; darkPanel1.Location = new System.Drawing.Point(1, 1); @@ -210,12 +201,12 @@ private void InitializeComponent() // butDeleteEventSet.Checked = false; butDeleteEventSet.Image = Properties.Resources.general_trash_16; - butDeleteEventSet.Location = new System.Drawing.Point(62, 3); + butDeleteEventSet.Location = new System.Drawing.Point(91, 3); butDeleteEventSet.Name = "butDeleteEventSet"; butDeleteEventSet.Size = new System.Drawing.Size(23, 23); butDeleteEventSet.TabIndex = 20; butDeleteEventSet.Tag = "AddNewRoom"; - toolTip.SetToolTip(butDeleteEventSet, "Delete selected event set"); + toolTip.SetToolTip(butDeleteEventSet, "Delete selected event set or folder"); butDeleteEventSet.Click += butDeleteEventSet_Click; // // butCloneEventSet @@ -223,13 +214,24 @@ private void InitializeComponent() butCloneEventSet.Checked = false; butCloneEventSet.DialogResult = DialogResult.Cancel; butCloneEventSet.Image = Properties.Resources.general_copy_16; - butCloneEventSet.Location = new System.Drawing.Point(33, 3); + butCloneEventSet.Location = new System.Drawing.Point(62, 3); butCloneEventSet.Name = "butCloneEventSet"; butCloneEventSet.Size = new System.Drawing.Size(23, 23); butCloneEventSet.TabIndex = 19; butCloneEventSet.Tag = "AddNewRoom"; toolTip.SetToolTip(butCloneEventSet, "Copy selected event set"); butCloneEventSet.Click += butCloneEventSet_Click; + // + // butNewFolder + // + butNewFolder.Checked = false; + butNewFolder.Image = Properties.Resources.general_Open_16; + butNewFolder.Location = new System.Drawing.Point(33, 3); + butNewFolder.Name = "butNewFolder"; + butNewFolder.Size = new System.Drawing.Size(23, 23); + butNewFolder.TabIndex = 27; + toolTip.SetToolTip(butNewFolder, "Add new folder"); + butNewFolder.Click += butNewFolder_Click; // // butNewEventSet // @@ -396,7 +398,6 @@ private void InitializeComponent() SizeGripStyle = SizeGripStyle.Hide; StartPosition = FormStartPosition.CenterParent; panelList.ResumeLayout(false); - ((System.ComponentModel.ISupportInitialize)dgvEvents).EndInit(); darkPanel1.ResumeLayout(false); panelEditor.ResumeLayout(false); splitContainer.Panel1.ResumeLayout(false); @@ -418,11 +419,12 @@ private void InitializeComponent() private DarkUI.Controls.DarkCheckBox cbActivatorStatics; private DarkUI.Controls.DarkCheckBox cbActivatorFlyBy; private DarkSectionPanel panelList; - private DarkDataGridView dgvEvents; + private DarkTreeView treeEvents; private Controls.TriggerManager triggerManager; private DarkPanel darkPanel1; private DarkButton butDeleteEventSet; private DarkButton butCloneEventSet; + private DarkButton butNewFolder; private DarkButton butNewEventSet; private DarkLabel lblActivators; private DarkButton butUnassignEventSet; diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index 6284e71754..a986dddea0 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -1,7 +1,8 @@ -using DarkUI.Forms; +using DarkUI.Collections; +using DarkUI.Controls; +using DarkUI.Forms; using System; using System.Collections.Generic; -using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; @@ -14,14 +15,8 @@ namespace TombEditor.Forms { public partial class FormEventSetEditor : DarkForm { - private enum SortMode - { - None, - Ascending, - Descending - } - - private SortMode _nextSortMode = SortMode.Ascending; + private const string _folderNodeType = "Folder"; + private const string _folderSeparator = "/"; private VolumeInstance _instance; private readonly Editor _editor; @@ -61,8 +56,12 @@ public EventSet SelectedSet if (_selectedSet == null) { - if (GenericMode && dgvEvents.Rows.Count > 0) - dgvEvents.Rows[0].Selected = true; + if (GenericMode && treeEvents.Nodes.Count > 0) + { + var firstEventNode = FindFirstEventSetNode(); + if (firstEventNode != null) + treeEvents.SelectNode(firstEventNode); + } else { ClearSelection(); @@ -71,20 +70,16 @@ public EventSet SelectedSet } else { - for (int i = 0; i < dgvEvents.Rows.Count; i++) + var node = FindNodeByEventSet(_selectedSet); + if (node != null) { - if (dgvEvents.Rows[i].Tag == _selectedSet) - { - ClearSelection(); - dgvEvents.Rows[i].Selected = true; - - LoadEventSetIntoUI(_selectedSet); - break; - } + ClearSelection(); + treeEvents.SelectNode(node); + treeEvents.EnsureVisible(); + LoadEventSetIntoUI(_selectedSet); } } - if (!GenericMode) _instance.EventSet = _selectedSet; } @@ -94,7 +89,6 @@ public EventSet SelectedSet public FormEventSetEditor(bool global, VolumeInstance instance = null) { InitializeComponent(); - dgvEvents.Columns.Add(new DataGridViewColumn(new DataGridViewTextBoxCell()) { HeaderText = "Event sets" }); _editor = Editor.Instance; _editor.EditorEventRaised += EditorEventRaised; @@ -121,6 +115,12 @@ public FormEventSetEditor(bool global, VolumeInstance instance = null) // Populate and select event set list PopulateEventSetList(); + // Only folder nodes can receive drag-drop children. + treeEvents.CanDropIntoNode = n => IsFolderNode(n); + + // Sync folders when user drags nodes in the tree. + treeEvents.NodesMoved += (s, args) => SyncFoldersFromTree(); + // Gray out UI by default, if event set list is empty if (_usedList.Count == 0) UpdateUI(); @@ -136,9 +136,6 @@ protected override void OnShown(EventArgs e) // Resize splitter splitContainer.SplitterDistance = _editor.Configuration.Window_FormEventSetEditor_SplitterDistance; - // Select event set, if volume exists. Must be in OnShown event because of DDGV bug which reselects - // first row after DDGV is drawn for the first time. - if (!GenericMode) SelectedSet = _instance.EventSet; } @@ -159,6 +156,8 @@ protected override void Dispose(bool disposing) if (DialogResult == DialogResult.Cancel) RestoreState(); + else + SyncFoldersFromTree(); _editor.EventSetsChange(); } @@ -216,11 +215,8 @@ public void UpdateVolume() public void ClearSelection() { - // HACK: Lock selection change to prevent DDGV from automatically selecting first row - // after clearing previous selection. - _lockSelectionChange = true; - dgvEvents.ClearSelection(); + treeEvents.SelectNodes(new List()); _lockSelectionChange = false; } @@ -251,13 +247,14 @@ private void SetupUI() private void UpdateUI() { bool eventSetSelected = SelectedSet != null; + bool folderSelected = treeEvents.SelectedNodes.Count > 0 && IsFolderNode(treeEvents.SelectedNodes[0]); tbName.Enabled = triggerManager.Enabled = cbEvents.Enabled = butUnassignEventSet.Enabled = - butCloneEventSet.Enabled = - butDeleteEventSet.Enabled = eventSetSelected; + butCloneEventSet.Enabled = eventSetSelected; + butDeleteEventSet.Enabled = eventSetSelected || folderSelected; cbActivatorLara.Enabled = cbActivatorNPC.Enabled = @@ -266,7 +263,7 @@ private void UpdateUI() cbActivatorFlyBy.Enabled = lblActivators.Enabled = eventSetSelected && !GlobalMode; - butSearch.Enabled = dgvEvents.Rows.Count > 0; + butSearch.Enabled = treeEvents.Nodes.Count > 0; } private void SetEventTooltip() @@ -376,18 +373,128 @@ private void PopulateEventSetList() { _lockSelectionChange = true; - dgvEvents.Rows.Clear(); + // Preserve expansion state of existing folder nodes before clearing. + var hadNodes = treeEvents.Nodes.Count > 0; + var expandedPaths = hadNodes ? new HashSet(treeEvents.GetAllNodes().Where(n => IsFolderNode(n) && n.Expanded).Select(n => GetFolderPath(n))) : null; + + treeEvents.Nodes.Clear(); foreach (var evtSet in _usedList) { - var row = new DataGridViewRow { Tag = evtSet }; - row.Cells.Add(new DataGridViewTextBoxCell() { Value = evtSet.Name }); - dgvEvents.Rows.Add(row); + var parentCollection = GetOrCreateFolderNodes(evtSet.Folder); + parentCollection.Add(new DarkTreeNode(evtSet.Name) { Tag = evtSet }); } + // Restore expansion state only when repopulating an existing tree. + if (expandedPaths != null) + foreach (var node in treeEvents.GetAllNodes().Where(n => IsFolderNode(n))) + node.Expanded = expandedPaths.Contains(GetFolderPath(node)); + _lockSelectionChange = false; } + private DarkTreeNode FindFirstEventSetNode() => treeEvents.GetAllNodes().FirstOrDefault(n => n.Tag is EventSet); + private DarkTreeNode FindNodeByEventSet(EventSet evtSet) => treeEvents.GetAllNodes().FirstOrDefault(n => n.Tag == evtSet); + private bool IsFolderNode(DarkTreeNode node) => node != null && node.NodeType as string == _folderNodeType; + + private ObservableList GetOrCreateFolderNodes(string path) + { + if (string.IsNullOrEmpty(path)) + return treeEvents.Nodes; + + var parts = path.Split(new[] { _folderSeparator }, StringSplitOptions.RemoveEmptyEntries); + var currentCollection = treeEvents.Nodes; + + foreach (var part in parts) + { + var existing = currentCollection.FirstOrDefault(n => IsFolderNode(n) && n.Text == part); + if (existing == null) + { + existing = new DarkTreeNode(part) { NodeType = _folderNodeType, Expanded = true }; + currentCollection.Add(existing); + } + currentCollection = existing.Nodes; + } + + return currentCollection; + } + + private string GetFolderPath(DarkTreeNode node) + { + var parts = new List(); + var current = node; + + while (current != null) + { + if (IsFolderNode(current)) + parts.Insert(0, current.Text); + current = current.ParentNode; + } + + return string.Join(_folderSeparator, parts); + } + + private void SyncFoldersFromTree() + { + foreach (var node in treeEvents.GetAllNodes()) + { + if (node.Tag is EventSet evtSet) + { + var parent = node.ParentNode; + evtSet.Folder = parent != null ? GetFolderPath(parent) : string.Empty; + } + } + + // Rebuild used list order from tree. + _usedList.Clear(); + foreach (var node in treeEvents.GetAllNodes()) + { + if (node.Tag is EventSet evtSet) + _usedList.Add(evtSet); + } + } + + private void CleanupEmptyFolders() + { + foreach (var evtSet in _usedList) + { + if (!string.IsNullOrEmpty(evtSet.Folder)) + { + bool hasOtherSets = _usedList.Any(s => s != evtSet && s.Folder == evtSet.Folder); + if (!hasOtherSets) + { + // Check if there are event sets in subfolders. + bool hasChildren = _usedList.Any(s => s != evtSet && s.Folder.StartsWith(evtSet.Folder + _folderSeparator)); + + if (!hasChildren) + { + // Check if there are sibling event sets in parent folder. + // Only clean up if event set itself no longer exists. + } + } + } + } + + // Remove folders with no event sets at all. + foreach (var evtSet in _usedList) + { + if (string.IsNullOrEmpty(evtSet.Folder)) + continue; + + var parts = evtSet.Folder.Split(new[] { _folderSeparator }, StringSplitOptions.RemoveEmptyEntries); + string checkPath = string.Empty; + + foreach (var part in parts) + { + checkPath = string.IsNullOrEmpty(checkPath) ? part : checkPath + _folderSeparator + part; + bool hasEvents = _usedList.Any(s => s.Folder == checkPath || s.Folder.StartsWith(checkPath + _folderSeparator)); + + if (!hasEvents) + evtSet.Folder = string.Empty; + } + } + } + private void ClearEventSetFromUI() { UpdateUI(); @@ -463,59 +570,47 @@ private void butCancel_Click(object sender, EventArgs e) Close(); } - private void dgvEvents_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e) - { - switch (_nextSortMode) - { - case SortMode.Ascending: - dgvEvents.AllowUserToDragDropRows = false; - - dgvEvents.Sort(dgvEvents.Columns[e.ColumnIndex], ListSortDirection.Ascending); - dgvEvents.Columns[e.ColumnIndex].HeaderText += " ▲"; - _nextSortMode = SortMode.Descending; - break; - - case SortMode.Descending: - dgvEvents.AllowUserToDragDropRows = false; - - dgvEvents.Sort(dgvEvents.Columns[e.ColumnIndex], ListSortDirection.Descending); - dgvEvents.Columns[e.ColumnIndex].HeaderText = dgvEvents.Columns[e.ColumnIndex].HeaderText.TrimEnd('▲', ' '); - dgvEvents.Columns[e.ColumnIndex].HeaderText += " ▼"; - _nextSortMode = SortMode.None; - break; - - default: - dgvEvents.AllowUserToDragDropRows = true; - - object selectedEventCache = dgvEvents.SelectedRows.Count > 0 ? dgvEvents.SelectedRows[0].Tag : null; - PopulateEventSetList(); - ClearSelection(); - - if (selectedEventCache != null) - foreach (DataGridViewRow row in dgvEvents.Rows) - if (row.Tag == selectedEventCache) - row.Selected = true; - - dgvEvents.Columns[e.ColumnIndex].HeaderText = dgvEvents.Columns[e.ColumnIndex].HeaderText.TrimEnd('▼', ' '); - _nextSortMode = SortMode.Ascending; - break; - } - } - - private void dgvEvents_SelectedIndicesChanged(object sender, EventArgs e) + private void treeEvents_SelectedNodesChanged(object sender, EventArgs e) { if (_lockSelectionChange) return; - var newEventSet = dgvEvents.SelectedRows.Count == 0 ? null : dgvEvents.SelectedRows[0].Tag as EventSet; - SelectedSet = newEventSet; + if (treeEvents.SelectedNodes.Count == 0) + { + SelectedSet = null; + UpdateUI(); + return; + } + + var selectedNode = treeEvents.SelectedNodes[0]; + if (selectedNode.Tag is EventSet evtSet) + { + _selectedSet = evtSet; + LoadEventSetIntoUI(evtSet); + } + else + { + _selectedSet = null; + ClearEventSetFromUI(); + } UpdateUI(); } private void butNewEventSet_Click(object sender, EventArgs e) { - var name = "New " + _mode + " event set " + (dgvEvents.Rows.Count + 1).ToString(); + var name = "New " + _mode + " event set " + (_usedList.Count + 1).ToString(); + + // Determine folder from selected node. + string folder = string.Empty; + if (treeEvents.SelectedNodes.Count > 0) + { + var selected = treeEvents.SelectedNodes[0]; + if (IsFolderNode(selected)) + folder = GetFolderPath(selected); + else if (selected.ParentNode != null && IsFolderNode(selected.ParentNode)) + folder = GetFolderPath(selected.ParentNode); + } EventSet newSet; @@ -524,6 +619,7 @@ private void butNewEventSet_Click(object sender, EventArgs e) newSet = new GlobalEventSet() { Name = name, + Folder = folder, LastUsedEvent = Event.GlobalEventTypes[_editor.Configuration.NodeEditor_DefaultGlobalEventToEdit] }; } @@ -532,6 +628,7 @@ private void butNewEventSet_Click(object sender, EventArgs e) newSet = new VolumeEventSet() { Name = name, + Folder = folder, LastUsedEvent = Event.VolumeEventTypes[_editor.Configuration.NodeEditor_DefaultEventToEdit] }; } @@ -554,6 +651,7 @@ private void butCloneEventSet_Click(object sender, EventArgs e) var clonedSet = SelectedSet.Clone(); clonedSet.Name = SelectedSet.Name + " (copy)"; + clonedSet.Folder = SelectedSet.Folder; _usedList.Add(clonedSet); PopulateEventSetList(); @@ -562,22 +660,93 @@ private void butCloneEventSet_Click(object sender, EventArgs e) private void butDeleteEventSet_Click(object sender, EventArgs e) { - int index = dgvEvents.SelectedRows.Count == 0 ? 0 : dgvEvents.SelectedRows[0].Index; - if (index == dgvEvents.Rows.Count - 1) - index--; + if (treeEvents.SelectedNodes.Count == 0) + return; - EditorActions.DeleteEventSet(SelectedSet); - PopulateEventSetList(); + var selectedNode = treeEvents.SelectedNodes[0]; - if (dgvEvents.Rows.Count > 0) + if (IsFolderNode(selectedNode)) { - SelectedSet = dgvEvents.Rows[index].Tag as EventSet; + var eventSetsInFolder = GetAllEventSetsUnder(selectedNode).ToList(); + + if (eventSetsInFolder.Count > 0) + { + var result = DarkMessageBox.Show(this, + "Delete " + eventSetsInFolder.Count + " event set" + (eventSetsInFolder.Count > 1 ? "s" : string.Empty) + " in folder '" + selectedNode.Text + "'?", + "Delete folder", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning); + + if (result == DialogResult.Cancel) + return; + + foreach (var evtSet in eventSetsInFolder) + EditorActions.DeleteEventSet(evtSet); + } + + PopulateEventSetList(); + + if (_usedList.Count > 0) + { + var firstNode = FindFirstEventSetNode(); + if (firstNode != null) + SelectedSet = firstNode.Tag as EventSet; + } + else + { + SelectedSet = null; + UpdateUI(); + } } - else + else if (selectedNode.Tag is EventSet) { - SelectedSet = null; - UpdateUI(); + var siblingEventNode = FindNextEventSetNode(selectedNode) ?? FindPrevEventSetNode(selectedNode); + + EditorActions.DeleteEventSet(SelectedSet); + PopulateEventSetList(); + + if (siblingEventNode?.Tag is EventSet nextSet && _usedList.Contains(nextSet)) + SelectedSet = nextSet; + else if (_usedList.Count > 0) + SelectedSet = _usedList[0]; + else + { + SelectedSet = null; + UpdateUI(); + } + } + } + + private IEnumerable GetAllEventSetsUnder(DarkTreeNode node) + { + if (node.Tag is EventSet evtSet) + yield return evtSet; + + foreach (var child in node.Nodes) + foreach (var set in GetAllEventSetsUnder(child)) + yield return set; + } + + private DarkTreeNode FindNextEventSetNode(DarkTreeNode current) + { + var all = treeEvents.GetAllNodes(); + int index = all.IndexOf(current); + for (int i = index + 1; i < all.Count; i++) + { + if (all[i].Tag is EventSet) + return all[i]; } + return null; + } + + private DarkTreeNode FindPrevEventSetNode(DarkTreeNode current) + { + var all = treeEvents.GetAllNodes(); + int index = all.IndexOf(current); + for (int i = index - 1; i >= 0; i--) + { + if (all[i].Tag is EventSet) + return all[i]; + } + return null; } private void butUnassignEventSet_Click(object sender, EventArgs e) @@ -595,7 +764,7 @@ private void cbActivators_CheckedChanged(object sender, EventArgs e) private void butSearch_Click(object sender, EventArgs e) { - var searchPopUp = new PopUpSearch(dgvEvents) { ShowAboveControl = true }; + var searchPopUp = new PopUpSearch(treeEvents) { ShowAboveControl = true }; searchPopUp.Show(this); } @@ -611,6 +780,19 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) switch (keyData) { + case Keys.Delete: + case Keys.Back: + if (treeEvents.ContainsFocus) + { + butDeleteEventSet_Click(butDeleteEventSet, EventArgs.Empty); + return true; + } + else + { + triggerManager.ProcessKey(keyData); + } + break; + case (Keys.Control | Keys.C): var copiedNodes = triggerManager.CopyNodes(false); if (copiedNodes.Count > 0) @@ -681,22 +863,76 @@ private void tbName_Validated(object sender, EventArgs e) } EditorActions.ReplaceEventSetNames(_usedList, SelectedSet.Name, tbName.Text); - dgvEvents.SelectedCells[0].Value = SelectedSet.Name = tbName.Text; + SelectedSet.Name = tbName.Text; + + var node = FindNodeByEventSet(SelectedSet); + if (node != null) + node.Text = tbName.Text; } - private void dgvEvents_DragDrop(object sender, DragEventArgs e) + private void butNewFolder_Click(object sender, EventArgs e) { - _usedList.Clear(); + string parentFolder = string.Empty; - foreach (DataGridViewRow row in dgvEvents.Rows) + if (treeEvents.SelectedNodes.Count > 0) { - if (row.Tag is not EventSet evtSet) - continue; + var selected = treeEvents.SelectedNodes[0]; + if (IsFolderNode(selected)) + parentFolder = GetFolderPath(selected); + else if (selected.ParentNode != null && IsFolderNode(selected.ParentNode)) + parentFolder = GetFolderPath(selected.ParentNode); + } + + using (var inputBox = new FormInputBox("New folder", "Enter folder name:", "New folder")) + { + if (inputBox.ShowDialog(this) != DialogResult.OK) + return; - _usedList.Add(evtSet); + string newFolderName = inputBox.Result.Trim(); + if (string.IsNullOrEmpty(newFolderName)) + return; + + string fullPath = string.IsNullOrEmpty(parentFolder) ? newFolderName : parentFolder + _folderSeparator + newFolderName; + GetOrCreateFolderNodes(fullPath); + + var newNode = treeEvents.GetAllNodes().LastOrDefault(n => IsFolderNode(n) && n.Text == newFolderName); + if (newNode != null) + { + treeEvents.SelectNode(newNode); + treeEvents.EnsureVisible(); + } } } + private void RenameFolder(DarkTreeNode node) + { + if (!IsFolderNode(node)) + return; + + using (var inputBox = new FormInputBox("Rename folder", "Enter new folder name:", node.Text)) + { + if (inputBox.ShowDialog(this) != DialogResult.OK) + return; + + string newName = inputBox.Result.Trim(); + if (!string.IsNullOrEmpty(newName) && newName != node.Text) + { + node.Text = newName; + SyncFoldersFromTree(); + } + } + } + + private void treeEvents_DoubleClick(object sender, EventArgs e) + { + if (treeEvents.SelectedNodes.Count == 0) + return; + + var node = treeEvents.SelectedNodes[0]; + if (IsFolderNode(node)) + RenameFolder(node); + } + private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) { if (Visible) diff --git a/TombLib/TombLib/LevelData/EventSet.cs b/TombLib/TombLib/LevelData/EventSet.cs index f186b245db..38dd3495e7 100644 --- a/TombLib/TombLib/LevelData/EventSet.cs +++ b/TombLib/TombLib/LevelData/EventSet.cs @@ -185,6 +185,7 @@ public abstract class EventSet : ICloneable, IEquatable { public EventType LastUsedEvent; public string Name; + public string Folder = string.Empty; // Every volume's events can be reduced to these three. // If resulting volume should be one-shot trigger, we'll only use "OnEnter" event. diff --git a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs index 9f1ccd824b..8a18b0c95d 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs @@ -94,6 +94,7 @@ internal static class Prj2Chunks /******/public static readonly ChunkId EventSet = ChunkId.FromString("TeEventSet"); /********/public static readonly ChunkId EventSetIndex = ChunkId.FromString("TeEventSetIndex"); /********/public static readonly ChunkId EventSetName = ChunkId.FromString("TeEventSetName"); + /********/public static readonly ChunkId EventSetFolder = ChunkId.FromString("TeEventSetFolder"); /********/public static readonly ChunkId EventSetLastUsedEventIndex = ChunkId.FromString("TeEventSetLUEI"); /********/public static readonly ChunkId EventSetActivators = ChunkId.FromString("TeEventSetActivators"); /********/public static readonly ChunkId EventSetOnEnter = ChunkId.FromString("TeEventSetOnEnter"); diff --git a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs index 04ba6a8d81..d08ba9e255 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs @@ -532,6 +532,8 @@ private static bool LoadLevelSettings(ChunkReader chunkIO, ChunkId idOuter, stri eventSetIndex = chunkIO.ReadChunkInt(chunkSize3); else if (id3 == Prj2Chunks.EventSetName) eventSet.Name = chunkIO.ReadChunkString(chunkSize3); + else if (id3 == Prj2Chunks.EventSetFolder) + eventSet.Folder = chunkIO.ReadChunkString(chunkSize3); else if (id3 == Prj2Chunks.EventSetLastUsedEventIndex) eventSet.LastUsedEvent = (EventType)chunkIO.ReadChunkInt(chunkSize3); else if (id3 == Prj2Chunks.EventSetActivators) diff --git a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs index 0bb1cadd9b..5b6634fb08 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs @@ -335,6 +335,8 @@ private static LevelSettingsIds WriteLevelSettings(ChunkWriter chunkIO, LevelSet { chunkIO.WriteChunkInt(Prj2Chunks.EventSetIndex, index); chunkIO.WriteChunkString(Prj2Chunks.EventSetName, set.Name ?? string.Empty); + if (!string.IsNullOrEmpty(set.Folder)) + chunkIO.WriteChunkString(Prj2Chunks.EventSetFolder, set.Folder); chunkIO.WriteChunkInt(Prj2Chunks.EventSetLastUsedEventIndex, (int)set.LastUsedEvent); if (!global) From 7ae6aee4f6cd5eb92d59f513703061ef1c8207a9 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sat, 11 Apr 2026 00:59:09 +0200 Subject: [PATCH 06/13] Fix problems with DarkTreeNode dragging --- DarkUI/DarkUI/Collections/ObservableList.cs | 7 + DarkUI/DarkUI/Controls/DarkTreeView.cs | 324 +++++++++++++------- TombEditor/Forms/FormEventSetEditor.cs | 167 +++++----- 3 files changed, 287 insertions(+), 211 deletions(-) diff --git a/DarkUI/DarkUI/Collections/ObservableList.cs b/DarkUI/DarkUI/Collections/ObservableList.cs index fce7f9a9e9..decfd5276c 100644 --- a/DarkUI/DarkUI/Collections/ObservableList.cs +++ b/DarkUI/DarkUI/Collections/ObservableList.cs @@ -67,6 +67,13 @@ protected virtual void Dispose(bool disposing) ItemsAdded?.Invoke(this, new ObservableListModified(list)); } + public new void Insert(int index, T item) + { + base.Insert(index, item); + + ItemsAdded?.Invoke(this, new ObservableListModified(new List { item })); + } + public new void Remove(T item) { base.Remove(item); diff --git a/DarkUI/DarkUI/Controls/DarkTreeView.cs b/DarkUI/DarkUI/Controls/DarkTreeView.cs index 02d2c5c436..51644eab46 100644 --- a/DarkUI/DarkUI/Controls/DarkTreeView.cs +++ b/DarkUI/DarkUI/Controls/DarkTreeView.cs @@ -50,6 +50,8 @@ public class DarkTreeView : DarkScrollView private DarkTreeNode _provisionalNode; private DarkTreeNode _dropNode; + private DropPosition _dropPosition; + private int _dropIndicatorY; private bool _provisionalDragging; private bool _mouseInClientArea; private List _dragNodes; @@ -57,6 +59,8 @@ public class DarkTreeView : DarkScrollView private readonly Color _borderColor = Colors.LightBorder; + private enum DropPosition { None, Before, After, Into } + #endregion #region Property Region @@ -210,6 +214,7 @@ private void Nodes_ItemsAdded(object sender, ObservableListModified e) { foreach (var node in e.Items) - { - node.ParentTree = this; - node.IsRoot = true; - - HookNodeEvents(node); - } + UnhookNodeEvents(node); UpdateNodes(); } @@ -292,25 +292,10 @@ protected override void OnMouseMove(MouseEventArgs e) } } - if (IsDragging) - { - if (_dropNode != null) - { - var rect = GetNodeFullRowArea(_dropNode); - if (!rect.Contains(OffsetMousePosition)) - { - _dropNode = null; - Invalidate(); - } - } - } - CheckHover(); if (IsDragging) - { HandleDrag(); - } base.OnMouseMove(e); } @@ -682,21 +667,7 @@ private void NodeMouseLeave(DarkTreeNode node) private void CheckNodeHover(DarkTreeNode node, Point location) { - if (IsDragging) - { - var rect = GetNodeFullRowArea(node); - if (rect.Contains(OffsetMousePosition)) - { - var newDropNode = _dragNodes.Contains(node) ? null : node; - - if (_dropNode != newDropNode) - { - _dropNode = newDropNode; - Invalidate(); - } - } - } - else + if (!IsDragging) { var hot = node.ExpandArea.Contains(location); if (node.ExpandAreaHot != hot) @@ -706,8 +677,11 @@ private void CheckNodeHover(DarkTreeNode node, Point location) } } - foreach (var childNode in node.Nodes) - CheckNodeHover(childNode, location); + if (node.Expanded) + { + foreach (var childNode in node.Nodes) + CheckNodeHover(childNode, location); + } } public void ExpandAllNodes() @@ -1098,35 +1072,115 @@ private void HandleDrag() if (!AllowMoveNodes) return; - var dropNode = _dropNode; + _mouseInClientArea = ClientRectangle.Contains(PointToClient(MousePosition)); - if (dropNode == null) + if (!_mouseInClientArea) + { + if (Cursor != Cursors.No) + Cursor = Cursors.No; + + ClearDropTarget(); + return; + } + + // Find the node the mouse is hovering over and compute drop position. + DarkTreeNode hitNode = null; + bool hitDragNode = false; + var position = DropPosition.None; + int indicatorY = 0; + + var allVisible = GetAllNodes(); + foreach (var node in allVisible) { - // Allow root-level drop when mouse is inside the control. - if (_mouseInClientArea) + var rect = GetNodeFullRowArea(node); + if (!rect.Contains(OffsetMousePosition)) + continue; + + if (_dragNodes.Contains(node)) + { + hitDragNode = true; + break; + } + + bool canDropInto = CanDropIntoNode == null || CanDropIntoNode(node); + int relativeY = OffsetMousePosition.Y - rect.Top; + int zoneSize = rect.Height / 4; + + if (relativeY < zoneSize) + { + position = DropPosition.Before; + indicatorY = rect.Top; + } + else if (relativeY > rect.Height - zoneSize) + { + position = DropPosition.After; + indicatorY = rect.Bottom; + } + else if (canDropInto) { - if (Cursor != Cursors.SizeAll) - Cursor = Cursors.SizeAll; + position = DropPosition.Into; + indicatorY = 0; } else { - if (Cursor != Cursors.No) - Cursor = Cursors.No; + // Not a valid drop-into target, treat center as before/after + // based on which half the mouse sits in. + if (relativeY < rect.Height / 2) + { + position = DropPosition.Before; + indicatorY = rect.Top; + } + else + { + position = DropPosition.After; + indicatorY = rect.Bottom; + } } - return; + hitNode = node; + break; } - if (!CanMoveNodes(_dragNodes, dropNode)) + // No valid drop target hit — determine fallback based on mouse position. + if (hitNode == null && !hitDragNode && _mouseInClientArea) { - if (Cursor != Cursors.No) - Cursor = Cursors.No; + var firstNonDrag = allVisible.FirstOrDefault(n => !_dragNodes.Contains(n)); + if (firstNonDrag != null) + { + var firstRect = GetNodeFullRowArea(firstNonDrag); + if (OffsetMousePosition.Y <= firstRect.Top) + { + // Mouse is above the first node — insert before it. + hitNode = firstNonDrag; + position = DropPosition.Before; + indicatorY = firstRect.Top; + } + else + { + // Mouse is below all nodes — append to root. + position = DropPosition.After; + indicatorY = ContentSize.Height; + } + } + } + // Validate drop target. + if (hitNode != null && !CanMoveNodes(_dragNodes, hitNode, position)) + { + ClearDropTarget(); + Cursor = Cursors.No; return; } - if (Cursor != Cursors.SizeAll) - Cursor = Cursors.SizeAll; + if (_dropNode != hitNode || _dropPosition != position || _dropIndicatorY != indicatorY) + { + _dropNode = hitNode; + _dropPosition = position; + _dropIndicatorY = indicatorY; + Invalidate(); + } + + Cursor = Cursors.SizeAll; } private void HandleDrop() @@ -1135,68 +1189,102 @@ private void HandleDrop() return; var dropNode = _dropNode; + var position = _dropPosition; - if (dropNode == null) + // Root-level drop: mouse in control area but not over any specific node. + if (dropNode == null && _mouseInClientArea) { - // Drop to root when mouse is inside the control but not over any node. - if (_mouseInClientArea) - { - var cachedSelectedNodes = SelectedNodes.ToList(); + MoveNodesToCollection(_dragNodes, Nodes, -1); + StopDrag(); + UpdateNodes(); + return; + } - foreach (var node in _dragNodes) - { - if (node.ParentNode == null) - Nodes.Remove(node); - else - node.ParentNode.Nodes.Remove(node); + if (dropNode == null || position == DropPosition.None) + { + StopDrag(); + return; + } - Nodes.Add(node); - } + switch (position) + { + case DropPosition.Into: + MoveNodesToCollection(_dragNodes, dropNode.Nodes, -1); + dropNode.Expanded = true; + break; - foreach (var node in cachedSelectedNodes) - SelectedNodes.Add(node); + case DropPosition.Before: + case DropPosition.After: + var targetCollection = dropNode.ParentNode != null ? dropNode.ParentNode.Nodes : Nodes; + int targetIndex = targetCollection.IndexOf(dropNode); - NodesMoved?.Invoke(this, EventArgs.Empty); - } + if (position == DropPosition.After) + targetIndex++; - StopDrag(); - UpdateNodes(); - return; + MoveNodesToCollection(_dragNodes, targetCollection, targetIndex); + break; } - if (CanMoveNodes(_dragNodes, dropNode, true)) + StopDrag(); + UpdateNodes(); + } + + private void MoveNodesToCollection(List nodes, ObservableList target, int insertIndex) + { + var cachedSelectedNodes = SelectedNodes.ToList(); + + foreach (var node in nodes) { - var cachedSelectedNodes = SelectedNodes.ToList(); + var sourceCollection = node.ParentNode != null ? node.ParentNode.Nodes : Nodes; - foreach (var node in _dragNodes) + // Adjust insert index when removing from the same collection before the target position. + if (sourceCollection == target && insertIndex >= 0) { - if (node.ParentNode == null) - Nodes.Remove(node); - else - node.ParentNode.Nodes.Remove(node); - - dropNode.Nodes.Add(node); + int currentIndex = sourceCollection.IndexOf(node); + if (currentIndex >= 0 && currentIndex < insertIndex) + insertIndex--; } - if (TreeViewNodeSorter != null) - dropNode.Nodes.Sort(TreeViewNodeSorter); + sourceCollection.Remove(node); - dropNode.Expanded = true; + if (insertIndex >= 0 && insertIndex <= target.Count) + target.Insert(insertIndex, node); + else + target.Add(node); - foreach (var node in cachedSelectedNodes) - SelectedNodes.Add(node); + if (insertIndex >= 0) + insertIndex++; + } + + if (TreeViewNodeSorter != null) + target.Sort(TreeViewNodeSorter); - NodesMoved?.Invoke(this, EventArgs.Empty); + foreach (var node in cachedSelectedNodes) + { + if (!SelectedNodes.Contains(node)) + SelectedNodes.Add(node); } - StopDrag(); - UpdateNodes(); + NodesMoved?.Invoke(this, EventArgs.Empty); + } + + private void ClearDropTarget() + { + if (_dropNode != null || _dropPosition != DropPosition.None) + { + _dropNode = null; + _dropPosition = DropPosition.None; + _dropIndicatorY = 0; + Invalidate(); + } } protected override void StopDrag() { _dragNodes = null; _dropNode = null; + _dropPosition = DropPosition.None; + _dropIndicatorY = 0; Cursor = Cursors.Default; @@ -1205,48 +1293,44 @@ protected override void StopDrag() base.StopDrag(); } - private bool CanMoveNodes(IEnumerable dragNodes, DarkTreeNode dropNode, bool isMoving = false) + private bool CanMoveNodes(IEnumerable dragNodes, DarkTreeNode dropNode, DropPosition position) { if (dropNode == null) return false; - if (CanDropIntoNode != null && !CanDropIntoNode(dropNode)) - { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move nodes into '{dropNode.Text}'. This node cannot contain other nodes.", Application.ProductName, MessageBoxIcon.Error); - - return false; - } - foreach (var node in dragNodes) { + // Cannot drop node onto itself. if (node == dropNode) - { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move {node.Text}. The destination folder is the same as the source folder.,", Application.ProductName, MessageBoxIcon.Error); - return false; - } - if (node.ParentNode != null && node.ParentNode == dropNode) + // For Before/After: check if the node is already at that exact position. + if (position == DropPosition.Before || position == DropPosition.After) { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move {node.Text}. The destination folder is the same as the source folder.", Application.ProductName, MessageBoxIcon.Error); + var targetCollection = dropNode.ParentNode != null ? dropNode.ParentNode.Nodes : Nodes; + var sourceCollection = node.ParentNode != null ? node.ParentNode.Nodes : Nodes; - return false; + if (sourceCollection == targetCollection) + { + int srcIdx = sourceCollection.IndexOf(node); + int dstIdx = targetCollection.IndexOf(dropNode); + if (position == DropPosition.Before && srcIdx == dstIdx - 1) + return false; + if (position == DropPosition.After && srcIdx == dstIdx + 1) + return false; + } } + // For Into: cannot drop into current parent. + if (position == DropPosition.Into && node.ParentNode == dropNode) + return false; + + // Cannot drop into a descendant of the dragged node. var parentNode = dropNode.ParentNode; while (parentNode != null) { if (node == parentNode) - { - if (isMoving) - DarkMessageBox.Show(this, $"Cannot move {node.Text}. The destination folder is a subfolder of the source folder.", Application.ProductName, MessageBoxIcon.Error); - return false; - } - parentNode = parentNode.ParentNode; } } @@ -1271,8 +1355,18 @@ protected override void OnPaint(PaintEventArgs e) protected override void PaintContent(Graphics g) { foreach (var node in Nodes) - { DrawNode(node, g); + + // Draw drop indicator line during drag operation. + if (IsDragging && _dropPosition != DropPosition.Into && _dropPosition != DropPosition.None) + { + int lineY = _dropIndicatorY; + int width = Math.Max(ContentSize.Width, Viewport.Width); + + using (var pen = new Pen(Colors.BlueHighlight, 2.0f)) + { + g.DrawLine(pen, 0, lineY, width, lineY); + } } } @@ -1298,7 +1392,7 @@ private void DrawNode(DarkTreeNode node, Graphics g) if (SelectedNodes.Count > 0 && SelectedNodes.Contains(node)) bgColor = Focused ? Colors.BlueSelection : Colors.GreySelection; - if (IsDragging && _dropNode == node) + if (IsDragging && _dropNode == node && _dropPosition == DropPosition.Into) bgColor = Focused ? Colors.BlueSelection : Colors.GreySelection; using (var b = new SolidBrush(bgColor)) diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index a986dddea0..401f0a7809 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -56,17 +56,8 @@ public EventSet SelectedSet if (_selectedSet == null) { - if (GenericMode && treeEvents.Nodes.Count > 0) - { - var firstEventNode = FindFirstEventSetNode(); - if (firstEventNode != null) - treeEvents.SelectNode(firstEventNode); - } - else - { - ClearSelection(); - ClearEventSetFromUI(); - } + ClearSelection(); + ClearEventSetFromUI(); } else { @@ -118,8 +109,12 @@ public FormEventSetEditor(bool global, VolumeInstance instance = null) // Only folder nodes can receive drag-drop children. treeEvents.CanDropIntoNode = n => IsFolderNode(n); - // Sync folders when user drags nodes in the tree. - treeEvents.NodesMoved += (s, args) => SyncFoldersFromTree(); + // Sync folder paths when user drags nodes in the tree. + treeEvents.NodesMoved += (s, args) => + { + SyncFoldersFromTree(); + RemoveEmptyFolderNodes(); + }; // Gray out UI by default, if event set list is empty if (_usedList.Count == 0) @@ -436,63 +431,40 @@ private string GetFolderPath(DarkTreeNode node) private void SyncFoldersFromTree() { - foreach (var node in treeEvents.GetAllNodes()) - { - if (node.Tag is EventSet evtSet) - { - var parent = node.ParentNode; - evtSet.Folder = parent != null ? GetFolderPath(parent) : string.Empty; - } - } - - // Rebuild used list order from tree. _usedList.Clear(); + foreach (var node in treeEvents.GetAllNodes()) { if (node.Tag is EventSet evtSet) + { + evtSet.Folder = node.ParentNode != null ? GetFolderPath(node.ParentNode) : string.Empty; _usedList.Add(evtSet); + } } } - private void CleanupEmptyFolders() + private void RemoveEmptyFolderNodes() { - foreach (var evtSet in _usedList) + bool removed; + do { - if (!string.IsNullOrEmpty(evtSet.Folder)) + removed = false; + foreach (var node in treeEvents.GetAllNodes()) { - bool hasOtherSets = _usedList.Any(s => s != evtSet && s.Folder == evtSet.Folder); - if (!hasOtherSets) + if (IsFolderNode(node) && node.Nodes.Count == 0) { - // Check if there are event sets in subfolders. - bool hasChildren = _usedList.Any(s => s != evtSet && s.Folder.StartsWith(evtSet.Folder + _folderSeparator)); - - if (!hasChildren) - { - // Check if there are sibling event sets in parent folder. - // Only clean up if event set itself no longer exists. - } + var parent = node.ParentNode; + if (parent != null) + parent.Nodes.Remove(node); + else + treeEvents.Nodes.Remove(node); + + removed = true; + break; } } } - - // Remove folders with no event sets at all. - foreach (var evtSet in _usedList) - { - if (string.IsNullOrEmpty(evtSet.Folder)) - continue; - - var parts = evtSet.Folder.Split(new[] { _folderSeparator }, StringSplitOptions.RemoveEmptyEntries); - string checkPath = string.Empty; - - foreach (var part in parts) - { - checkPath = string.IsNullOrEmpty(checkPath) ? part : checkPath + _folderSeparator + part; - bool hasEvents = _usedList.Any(s => s.Folder == checkPath || s.Folder.StartsWith(checkPath + _folderSeparator)); - - if (!hasEvents) - evtSet.Folder = string.Empty; - } - } + while (removed); } private void ClearEventSetFromUI() @@ -682,73 +654,76 @@ private void butDeleteEventSet_Click(object sender, EventArgs e) EditorActions.DeleteEventSet(evtSet); } - PopulateEventSetList(); - - if (_usedList.Count > 0) - { - var firstNode = FindFirstEventSetNode(); - if (firstNode != null) - SelectedSet = firstNode.Tag as EventSet; - } + // Remove the folder node from the tree. + if (selectedNode.ParentNode != null) + selectedNode.ParentNode.Nodes.Remove(selectedNode); else - { - SelectedSet = null; - UpdateUI(); - } + treeEvents.Nodes.Remove(selectedNode); + + SyncFoldersFromTree(); + SelectFirstAvailableEventSet(); } else if (selectedNode.Tag is EventSet) { - var siblingEventNode = FindNextEventSetNode(selectedNode) ?? FindPrevEventSetNode(selectedNode); + var nextSet = FindAdjacentEventSet(selectedNode); EditorActions.DeleteEventSet(SelectedSet); PopulateEventSetList(); - if (siblingEventNode?.Tag is EventSet nextSet && _usedList.Contains(nextSet)) + if (nextSet != null && _usedList.Contains(nextSet)) SelectedSet = nextSet; - else if (_usedList.Count > 0) - SelectedSet = _usedList[0]; else - { - SelectedSet = null; - UpdateUI(); - } + SelectFirstAvailableEventSet(); } } - private IEnumerable GetAllEventSetsUnder(DarkTreeNode node) + private void SelectFirstAvailableEventSet() { - if (node.Tag is EventSet evtSet) - yield return evtSet; - - foreach (var child in node.Nodes) - foreach (var set in GetAllEventSetsUnder(child)) - yield return set; + if (_usedList.Count > 0) + { + var firstNode = FindFirstEventSetNode(); + if (firstNode != null) + SelectedSet = firstNode.Tag as EventSet; + else + SelectedSet = null; + } + else + { + SelectedSet = null; + } } - private DarkTreeNode FindNextEventSetNode(DarkTreeNode current) + private EventSet FindAdjacentEventSet(DarkTreeNode current) { - var all = treeEvents.GetAllNodes(); - int index = all.IndexOf(current); - for (int i = index + 1; i < all.Count; i++) + var allNodes = treeEvents.GetAllNodes(); + int index = allNodes.IndexOf(current); + + // Look forward first, then backward. + for (int i = index + 1; i < allNodes.Count; i++) { - if (all[i].Tag is EventSet) - return all[i]; + if (allNodes[i].Tag is EventSet evtSet) + return evtSet; } - return null; - } - private DarkTreeNode FindPrevEventSetNode(DarkTreeNode current) - { - var all = treeEvents.GetAllNodes(); - int index = all.IndexOf(current); for (int i = index - 1; i >= 0; i--) { - if (all[i].Tag is EventSet) - return all[i]; + if (allNodes[i].Tag is EventSet evtSet) + return evtSet; } + return null; } + private IEnumerable GetAllEventSetsUnder(DarkTreeNode node) + { + if (node.Tag is EventSet evtSet) + yield return evtSet; + + foreach (var child in node.Nodes) + foreach (var set in GetAllEventSetsUnder(child)) + yield return set; + } + private void butUnassignEventSet_Click(object sender, EventArgs e) { if (GenericMode) From 462294b5e47fac5be392945ee39044aec687c0b7 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sat, 11 Apr 2026 01:16:04 +0200 Subject: [PATCH 07/13] UI fixes for leaf nodes --- DarkUI/DarkUI/Controls/DarkTreeView.cs | 6 ++++-- TombEditor/Forms/FormEventSetEditor.cs | 6 +----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/DarkUI/DarkUI/Controls/DarkTreeView.cs b/DarkUI/DarkUI/Controls/DarkTreeView.cs index 51644eab46..51686715f9 100644 --- a/DarkUI/DarkUI/Controls/DarkTreeView.cs +++ b/DarkUI/DarkUI/Controls/DarkTreeView.cs @@ -573,8 +573,10 @@ private void UpdateNodeBounds(DarkTreeNode node, int yOffset, int indent) if (ShowIcons) node.IconArea = new Rectangle(node.ExpandArea.Right + 2, iconTop, IconSize, IconSize); - else + else if (node.Nodes.Count > 0) node.IconArea = new Rectangle(node.ExpandArea.Right, iconTop, 0, 0); + else + node.IconArea = new Rectangle(node.ExpandArea.Left, iconTop, 0, 0); using (var g = CreateGraphics()) { @@ -1360,7 +1362,7 @@ protected override void PaintContent(Graphics g) // Draw drop indicator line during drag operation. if (IsDragging && _dropPosition != DropPosition.Into && _dropPosition != DropPosition.None) { - int lineY = _dropIndicatorY; + int lineY = Math.Max(2, _dropIndicatorY); int width = Math.Max(ContentSize.Width, Viewport.Width); using (var pen = new Pen(Colors.BlueHighlight, 2.0f)) diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index 401f0a7809..6713b5623e 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -110,11 +110,7 @@ public FormEventSetEditor(bool global, VolumeInstance instance = null) treeEvents.CanDropIntoNode = n => IsFolderNode(n); // Sync folder paths when user drags nodes in the tree. - treeEvents.NodesMoved += (s, args) => - { - SyncFoldersFromTree(); - RemoveEmptyFolderNodes(); - }; + treeEvents.NodesMoved += (s, args) => SyncFoldersFromTree(); // Gray out UI by default, if event set list is empty if (_usedList.Count == 0) From d10ff811241fb558e325a8d2a161c41881e5c415 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:58:42 +0200 Subject: [PATCH 08/13] Address Copilot comments --- .../Controls/VisualScripting/NodeEditor.cs | 14 ++++++++------ .../TombLib/Catalogs/TEN Node Catalogs/Readme.md | 6 +++--- TombLib/TombLib/Utils/ScriptingUtils.cs | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index 2e3ce481f6..a84ab45ad3 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -921,13 +921,15 @@ private void DrawHeader(PaintEventArgs e, VisibleNodeBase node) if (unsupported) { var matrix = new System.Drawing.Imaging.ColorMatrix { Matrix33 = LabelOpacity, Matrix22 = 0.0f }; - var attributes = new System.Drawing.Imaging.ImageAttributes(); - attributes.SetColorMatrix(matrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); + using (var attributes = new System.Drawing.Imaging.ImageAttributes()) + { + attributes.SetColorMatrix(matrix, System.Drawing.Imaging.ColorMatrixFlag.Default, System.Drawing.Imaging.ColorAdjustType.Bitmap); - var icon = Properties.Resources.general_Warning_16; - var iconRect = new Rectangle(rect.X, rect.Y + (rect.Height - icon.Height), icon.Width, icon.Height); - e.Graphics.DrawImage(icon, iconRect, 0, 0, icon.Width, icon.Height, GraphicsUnit.Pixel, attributes); - iconOffset = icon.Width + 2; + var icon = Properties.Resources.general_Warning_16; + var iconRect = new Rectangle(rect.X, rect.Y + (rect.Height - icon.Height), icon.Width, icon.Height); + e.Graphics.DrawImage(icon, iconRect, 0, 0, icon.Width, icon.Height, GraphicsUnit.Pixel, attributes); + iconOffset = icon.Width + 2; + } } var textRect = new Rectangle(rect.X + iconOffset, rect.Y, rect.Width - iconOffset, rect.Height); diff --git a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md index b2b8b3db8d..3ff3058e60 100644 --- a/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md +++ b/TombLib/TombLib/Catalogs/TEN Node Catalogs/Readme.md @@ -41,12 +41,12 @@ Comment metadata entry reference (metadata block is indicated by a keyword which - **!Section "SECTION"** - this will define where the node will be found inside Tomb Editor. - - **!Supported "TYPE" "TYPE" "..." - defines supported event types for a given node. Each event type should be - enclosed in quotes. If event is not supported by a node, it will display a warning message when misplaced. + - **!Supported "TYPE" "TYPE" "..."** - defines supported event types for a given node. Each event type should be + enclosed in quotes. If an event is not supported by a node, it will display a warning message when misplaced. Possible event types are: **OnVolumeEnter, OnVolumeInside, OnVolumeLeave, OnLoop, OnLoadGame, OnSaveGame, OnLevelStart, OnLevelEnd, OnUseItem, OnFreeze**. - - **!Unsupported "TYPE" "TYPE" "..." - defines unsupported event types for a given node. Acts in an opposite way + - **!Unsupported "TYPE" "TYPE" "..."** - defines unsupported event types for a given node. Acts in an opposite way to `!Supported`. Possible event types are equal to `!Supported`. - **!Arguments "ARGDESC1" "ARGDESC2" "ARGDESC..."** - infinite amount of args, with **ARGDESC** parameters diff --git a/TombLib/TombLib/Utils/ScriptingUtils.cs b/TombLib/TombLib/Utils/ScriptingUtils.cs index adad2e4a98..1313654a39 100644 --- a/TombLib/TombLib/Utils/ScriptingUtils.cs +++ b/TombLib/TombLib/Utils/ScriptingUtils.cs @@ -272,10 +272,10 @@ private static List GetAllNodeFunctions(string path, List targetList) { - var values = TextExtensions.ExtractValues(comment.Substring(tagId.Length, comment.Length - tagId.Length)); + var values = TextExtensions.ExtractValues(comment.Substring(tagId.Length)); foreach (var v in values) { - if (Enum.TryParse(v.Trim(), out EventType eventType)) + if (Enum.TryParse(v.Trim(), true, out EventType eventType) && !targetList.Contains(eventType)) targetList.Add(eventType); } } From 002df00618814bd50599d296376d8ed67754e633 Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:27:47 +0200 Subject: [PATCH 09/13] Remember state of the collapsed/opened folders, add same folder name safeguard --- TombEditor/Forms/FormEventSetEditor.cs | 112 ++++++++++++++------- TombLib/TombLib/LevelData/IO/Prj2Chunks.cs | 3 + TombLib/TombLib/LevelData/IO/Prj2Loader.cs | 16 +++ TombLib/TombLib/LevelData/IO/Prj2Writer.cs | 13 +++ TombLib/TombLib/LevelData/LevelSettings.cs | 2 + 5 files changed, 111 insertions(+), 35 deletions(-) diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index 6713b5623e..e516a23bdb 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -40,6 +40,9 @@ public partial class FormEventSetEditor : DarkForm public bool GlobalMode => _usedList == _editor.Level.Settings.GlobalEventSets; public bool GenericMode => GlobalMode || _instance == null; + private HashSet _backupCollapsedFolders; + private HashSet _сollapsedFolders => GlobalMode ? _editor.Level.Settings.CollapsedGlobalEventSetFolders : _editor.Level.Settings.CollapsedVolumeEventSetFolders; + public EventSet SelectedSet { get @@ -112,6 +115,10 @@ public FormEventSetEditor(bool global, VolumeInstance instance = null) // Sync folder paths when user drags nodes in the tree. treeEvents.NodesMoved += (s, args) => SyncFoldersFromTree(); + // Persist folder expansion state to the level file. + treeEvents.AfterNodeExpand += (s, args) => SyncCollapsedFolders(); + treeEvents.AfterNodeCollapse += (s, args) => SyncCollapsedFolders(); + // Gray out UI by default, if event set list is empty if (_usedList.Count == 0) UpdateUI(); @@ -321,10 +328,16 @@ private void BackupState() _backupEventSetList = new List(); foreach (var evtSet in _usedList) _backupEventSetList.Add(evtSet.Clone()); + + _backupCollapsedFolders = new HashSet(_сollapsedFolders); } private void RestoreState() { + _сollapsedFolders.Clear(); + foreach (var path in _backupCollapsedFolders) + _сollapsedFolders.Add(path); + if (GlobalMode) { _editor.Level.Settings.GlobalEventSets = _backupEventSetList; @@ -364,10 +377,6 @@ private void PopulateEventSetList() { _lockSelectionChange = true; - // Preserve expansion state of existing folder nodes before clearing. - var hadNodes = treeEvents.Nodes.Count > 0; - var expandedPaths = hadNodes ? new HashSet(treeEvents.GetAllNodes().Where(n => IsFolderNode(n) && n.Expanded).Select(n => GetFolderPath(n))) : null; - treeEvents.Nodes.Clear(); foreach (var evtSet in _usedList) @@ -376,10 +385,10 @@ private void PopulateEventSetList() parentCollection.Add(new DarkTreeNode(evtSet.Name) { Tag = evtSet }); } - // Restore expansion state only when repopulating an existing tree. - if (expandedPaths != null) - foreach (var node in treeEvents.GetAllNodes().Where(n => IsFolderNode(n))) - node.Expanded = expandedPaths.Contains(GetFolderPath(node)); + // Apply stored expansion state after all children are in place. + // DarkTreeNode.Expanded setter is a no-op on empty nodes, so this must be done post-populate. + foreach (var node in treeEvents.GetAllNodes().Where(n => IsFolderNode(n))) + node.Expanded = !_сollapsedFolders.Contains(GetFolderPath(node)); _lockSelectionChange = false; } @@ -387,6 +396,8 @@ private void PopulateEventSetList() private DarkTreeNode FindFirstEventSetNode() => treeEvents.GetAllNodes().FirstOrDefault(n => n.Tag is EventSet); private DarkTreeNode FindNodeByEventSet(EventSet evtSet) => treeEvents.GetAllNodes().FirstOrDefault(n => n.Tag == evtSet); private bool IsFolderNode(DarkTreeNode node) => node != null && node.NodeType as string == _folderNodeType; + private bool FolderNameExists(ObservableList collection, string name, DarkTreeNode exclude = null) => + collection.Any(n => IsFolderNode(n) && n != exclude && string.Equals(n.Text, name, StringComparison.OrdinalIgnoreCase)); private ObservableList GetOrCreateFolderNodes(string path) { @@ -401,7 +412,7 @@ private ObservableList GetOrCreateFolderNodes(string path) var existing = currentCollection.FirstOrDefault(n => IsFolderNode(n) && n.Text == part); if (existing == null) { - existing = new DarkTreeNode(part) { NodeType = _folderNodeType, Expanded = true }; + existing = new DarkTreeNode(part) { NodeType = _folderNodeType }; currentCollection.Add(existing); } currentCollection = existing.Nodes; @@ -439,6 +450,18 @@ private void SyncFoldersFromTree() } } + + private void SyncCollapsedFolders() + { + _сollapsedFolders.Clear(); + + foreach (var node in treeEvents.GetAllNodes()) + { + if (IsFolderNode(node) && !node.Expanded) + _сollapsedFolders.Add(GetFolderPath(node)); + } + } + private void RemoveEmptyFolderNodes() { bool removed; @@ -854,24 +877,19 @@ private void butNewFolder_Click(object sender, EventArgs e) parentFolder = GetFolderPath(selected.ParentNode); } - using (var inputBox = new FormInputBox("New folder", "Enter folder name:", "New folder")) - { - if (inputBox.ShowDialog(this) != DialogResult.OK) - return; - - string newFolderName = inputBox.Result.Trim(); - if (string.IsNullOrEmpty(newFolderName)) - return; + var parentCollection = GetOrCreateFolderNodes(parentFolder); + string newFolderName = PromptUniqueFolderName("New folder", "Enter folder name:", "New folder", parentCollection); + if (newFolderName == null) + return; - string fullPath = string.IsNullOrEmpty(parentFolder) ? newFolderName : parentFolder + _folderSeparator + newFolderName; - GetOrCreateFolderNodes(fullPath); + string fullPath = string.IsNullOrEmpty(parentFolder) ? newFolderName : parentFolder + _folderSeparator + newFolderName; + GetOrCreateFolderNodes(fullPath); - var newNode = treeEvents.GetAllNodes().LastOrDefault(n => IsFolderNode(n) && n.Text == newFolderName); - if (newNode != null) - { - treeEvents.SelectNode(newNode); - treeEvents.EnsureVisible(); - } + var newNode = treeEvents.GetAllNodes().LastOrDefault(n => IsFolderNode(n) && n.Text == newFolderName); + if (newNode != null) + { + treeEvents.SelectNode(newNode); + treeEvents.EnsureVisible(); } } @@ -880,16 +898,36 @@ private void RenameFolder(DarkTreeNode node) if (!IsFolderNode(node)) return; - using (var inputBox = new FormInputBox("Rename folder", "Enter new folder name:", node.Text)) - { - if (inputBox.ShowDialog(this) != DialogResult.OK) - return; + var siblings = node.ParentNode?.Nodes ?? treeEvents.Nodes; + string newName = PromptUniqueFolderName("Rename folder", "Enter new folder name:", node.Text, siblings, node); + if (newName == null || newName == node.Text) + return; + + node.Text = newName; + SyncFoldersFromTree(); + } - string newName = inputBox.Result.Trim(); - if (!string.IsNullOrEmpty(newName) && newName != node.Text) + // Shows a name-input dialog in a loop until the user enters a name that doesn't conflict with + // an existing folder in the given collection, or cancels. Returns null on cancel or empty input. + private string PromptUniqueFolderName(string title, string prompt, string initialValue, ObservableList collection, DarkTreeNode exclude = null) + { + string current = initialValue; + while (true) + { + using (var inputBox = new FormInputBox(title, prompt, current)) { - node.Text = newName; - SyncFoldersFromTree(); + if (inputBox.ShowDialog(this) != DialogResult.OK) + return null; + + string name = inputBox.Result.Trim(); + if (string.IsNullOrEmpty(name)) + return null; + + if (!FolderNameExists(collection, name, exclude)) + return name; + + DarkMessageBox.Show(this, "A folder with that name already exists. Specify a different name.", title, MessageBoxButtons.OK, MessageBoxIcon.Warning); + current = name; } } } @@ -900,8 +938,12 @@ private void treeEvents_DoubleClick(object sender, EventArgs e) return; var node = treeEvents.SelectedNodes[0]; - if (IsFolderNode(node)) - RenameFolder(node); + if (!IsFolderNode(node)) + return; + + // DarkTreeView already toggled Expanded via OnMouseDoubleClick, undo that. + node.Expanded = !node.Expanded; + RenameFolder(node); } private void splitContainer_SplitterMoved(object sender, SplitterEventArgs e) diff --git a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs index 3224d58c82..3b410ed0d7 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Chunks.cs @@ -249,5 +249,8 @@ internal static class Prj2Chunks /**/public static readonly ChunkId Palette = ChunkId.FromString("TePalette"); /**/public static readonly ChunkId Favorites = ChunkId.FromString("TeFavorites"); /****/public static readonly ChunkId Favorite = ChunkId.FromString("TeFavorite"); + /**/public static readonly ChunkId CollapsedGlobalEventSetFolders = ChunkId.FromString("TeCollapsedGlbEvtFolders"); + /**/public static readonly ChunkId CollapsedVolumeEventSetFolders = ChunkId.FromString("TeCollapsedVolEvtFolders"); + /****/public static readonly ChunkId CollapsedEventSetFolder = ChunkId.FromString("TeCollapsedEvtFolder"); } } diff --git a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs index 2604c3b160..f052744ac7 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Loader.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Loader.cs @@ -733,6 +733,22 @@ private static bool LoadLevelSettings(ChunkReader chunkIO, ChunkId idOuter, stri else return false; }); } + else if (id == Prj2Chunks.CollapsedGlobalEventSetFolders || + id == Prj2Chunks.CollapsedVolumeEventSetFolders) + { + var target = id == Prj2Chunks.CollapsedGlobalEventSetFolders ? settings.CollapsedGlobalEventSetFolders : settings.CollapsedVolumeEventSetFolders; + + target.Clear(); + chunkIO.ReadChunks((id2, chunkSize2) => + { + if (id2 == Prj2Chunks.CollapsedEventSetFolder) + { + target.Add(chunkIO.ReadChunkString(chunkSize2)); + return true; + } + else return false; + }); + } else return false; return true; diff --git a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs index 6ea6a28cc7..7e1a47ff75 100644 --- a/TombLib/TombLib/LevelData/IO/Prj2Writer.cs +++ b/TombLib/TombLib/LevelData/IO/Prj2Writer.cs @@ -403,6 +403,19 @@ private static LevelSettingsIds WriteLevelSettings(ChunkWriter chunkIO, LevelSet chunkIO.WriteChunkEnd(); } } + + foreach (bool global in new[] { true, false }) + { + var collapsed = global ? settings.CollapsedGlobalEventSetFolders : settings.CollapsedVolumeEventSetFolders; + + if (collapsed.Count > 0) + using (var chunkCollapsed = chunkIO.WriteChunk(global ? Prj2Chunks.CollapsedGlobalEventSetFolders : Prj2Chunks.CollapsedVolumeEventSetFolders, long.MaxValue)) + { + foreach (var path in collapsed) + chunkIO.WriteChunkString(Prj2Chunks.CollapsedEventSetFolder, path); + chunkIO.WriteChunkEnd(); + } + } chunkIO.WriteChunkEnd(); } diff --git a/TombLib/TombLib/LevelData/LevelSettings.cs b/TombLib/TombLib/LevelData/LevelSettings.cs index 97fa8545fa..64967ba7e4 100644 --- a/TombLib/TombLib/LevelData/LevelSettings.cs +++ b/TombLib/TombLib/LevelData/LevelSettings.cs @@ -204,6 +204,8 @@ public List GlobalSoundMap public List AnimatedTextureSets { get; set; } = new List(); public List GlobalEventSets { get; set; } = new List(); public List VolumeEventSets { get; set; } = new List(); + public HashSet CollapsedGlobalEventSetFolders { get; set; } = new HashSet(); + public HashSet CollapsedVolumeEventSetFolders { get; set; } = new HashSet(); public List Palette { get; set; } = LoadPalette(ResourcesC.ResourcesC.palette); public HashSet Favorites { get; set; } = new HashSet(StringComparer.OrdinalIgnoreCase); From 2b79a91c5ba9c377bcc48e430d79669aff2cb93d Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:41:08 +0200 Subject: [PATCH 10/13] Preserve opened folders on new event set creation --- TombEditor/Forms/FormEventSetEditor.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index e516a23bdb..953752a639 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -377,6 +377,7 @@ private void PopulateEventSetList() { _lockSelectionChange = true; + var collapsed = new HashSet(_сollapsedFolders); treeEvents.Nodes.Clear(); foreach (var evtSet in _usedList) @@ -385,10 +386,8 @@ private void PopulateEventSetList() parentCollection.Add(new DarkTreeNode(evtSet.Name) { Tag = evtSet }); } - // Apply stored expansion state after all children are in place. - // DarkTreeNode.Expanded setter is a no-op on empty nodes, so this must be done post-populate. foreach (var node in treeEvents.GetAllNodes().Where(n => IsFolderNode(n))) - node.Expanded = !_сollapsedFolders.Contains(GetFolderPath(node)); + node.Expanded = !collapsed.Contains(GetFolderPath(node)); _lockSelectionChange = false; } From 353dff3008a2ec295688ac2b5ca956a49cfbb05e Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:46:21 +0200 Subject: [PATCH 11/13] Update Changes.txt --- Installer/Changes.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Installer/Changes.txt b/Installer/Changes.txt index a448db8e8b..d908084f63 100644 --- a/Installer/Changes.txt +++ b/Installer/Changes.txt @@ -6,6 +6,8 @@ Tomb Editor: * Added Content Browser tool window for more streamlined object management. * Added Flyby Timeline control to edit and preview flyby sequences. * Added "Preview flyby sequence" and "Preview camera" context menus for cameras. + * Added ability to group event sets into categories. + * Added indication of the nodes not supported by the current event type. Version 1.11 ============ From 0b1c37b5bccbbeb33c9859dbdb115be9cd18083c Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 17 May 2026 19:37:37 +0200 Subject: [PATCH 12/13] Address Copilot comments --- TombEditor/Forms/FormEventSetEditor.cs | 16 ++++++++-------- TombLib/TombLib/LevelData/LevelSettings.cs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index 953752a639..48f54d968a 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -41,7 +41,7 @@ public partial class FormEventSetEditor : DarkForm public bool GenericMode => GlobalMode || _instance == null; private HashSet _backupCollapsedFolders; - private HashSet _сollapsedFolders => GlobalMode ? _editor.Level.Settings.CollapsedGlobalEventSetFolders : _editor.Level.Settings.CollapsedVolumeEventSetFolders; + private HashSet _collapsedFolders => GlobalMode ? _editor.Level.Settings.CollapsedGlobalEventSetFolders : _editor.Level.Settings.CollapsedVolumeEventSetFolders; public EventSet SelectedSet { @@ -152,7 +152,7 @@ protected override void Dispose(bool disposing) { _editor.EditorEventRaised -= EditorEventRaised; - if (DialogResult == DialogResult.Cancel) + if (DialogResult != DialogResult.OK) RestoreState(); else SyncFoldersFromTree(); @@ -329,14 +329,14 @@ private void BackupState() foreach (var evtSet in _usedList) _backupEventSetList.Add(evtSet.Clone()); - _backupCollapsedFolders = new HashSet(_сollapsedFolders); + _backupCollapsedFolders = new HashSet(_collapsedFolders); } private void RestoreState() { - _сollapsedFolders.Clear(); + _collapsedFolders.Clear(); foreach (var path in _backupCollapsedFolders) - _сollapsedFolders.Add(path); + _collapsedFolders.Add(path); if (GlobalMode) { @@ -377,7 +377,7 @@ private void PopulateEventSetList() { _lockSelectionChange = true; - var collapsed = new HashSet(_сollapsedFolders); + var collapsed = new HashSet(_collapsedFolders); treeEvents.Nodes.Clear(); foreach (var evtSet in _usedList) @@ -452,12 +452,12 @@ private void SyncFoldersFromTree() private void SyncCollapsedFolders() { - _сollapsedFolders.Clear(); + _collapsedFolders.Clear(); foreach (var node in treeEvents.GetAllNodes()) { if (IsFolderNode(node) && !node.Expanded) - _сollapsedFolders.Add(GetFolderPath(node)); + _collapsedFolders.Add(GetFolderPath(node)); } } diff --git a/TombLib/TombLib/LevelData/LevelSettings.cs b/TombLib/TombLib/LevelData/LevelSettings.cs index 64967ba7e4..8632ce39c2 100644 --- a/TombLib/TombLib/LevelData/LevelSettings.cs +++ b/TombLib/TombLib/LevelData/LevelSettings.cs @@ -7,8 +7,6 @@ using System.Numerics; using System.Reflection; using System.Text.RegularExpressions; -using TombLib.LevelData.IO; -using TombLib.NG; using TombLib.Utils; using TombLib.Wad; using ImportedGeometryUpdateInfo = System.Collections.Generic.KeyValuePair; @@ -245,7 +243,9 @@ public LevelSettings Clone() result.AnimatedTextureSets = AnimatedTextureSets.ConvertAll(set => set.Clone()); result.ImportedGeometries = ImportedGeometries.ConvertAll(geometry => geometry.Clone()); result.AutoStaticMeshMerges = AutoStaticMeshMerges.ConvertAll(entry => entry.Clone()); - return result; + result.CollapsedGlobalEventSetFolders = new HashSet(CollapsedGlobalEventSetFolders); + result.CollapsedVolumeEventSetFolders = new HashSet(CollapsedVolumeEventSetFolders); + return result; } object ICloneable.Clone() From ef86b994a5328b6d741f543790fdd68a276a8f3a Mon Sep 17 00:00:00 2001 From: Lwmte <3331699+Lwmte@users.noreply.github.com> Date: Sun, 17 May 2026 19:48:45 +0200 Subject: [PATCH 13/13] Formatting --- TombEditor/Forms/FormEventSetEditor.cs | 15 +++++++-------- .../Controls/VisualScripting/NodeEditor.cs | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/TombEditor/Forms/FormEventSetEditor.cs b/TombEditor/Forms/FormEventSetEditor.cs index dc7b049a2a..f00d2414fd 100644 --- a/TombEditor/Forms/FormEventSetEditor.cs +++ b/TombEditor/Forms/FormEventSetEditor.cs @@ -461,7 +461,6 @@ private void SyncFoldersFromTree() } } - private void SyncCollapsedFolders() { _collapsedFolders.Clear(); @@ -604,7 +603,7 @@ private void butNewEventSet_Click(object sender, EventArgs e) var name = "New " + _mode + " event set " + (_usedList.Count + 1).ToString(); // Determine folder from selected node. - string folder = string.Empty; + var folder = string.Empty; if (treeEvents.SelectedNodes.Count > 0) { var selected = treeEvents.SelectedNodes[0]; @@ -877,7 +876,7 @@ private void tbName_Validated(object sender, EventArgs e) private void butNewFolder_Click(object sender, EventArgs e) { - string parentFolder = string.Empty; + var parentFolder = string.Empty; if (treeEvents.SelectedNodes.Count > 0) { @@ -889,11 +888,11 @@ private void butNewFolder_Click(object sender, EventArgs e) } var parentCollection = GetOrCreateFolderNodes(parentFolder); - string newFolderName = PromptUniqueFolderName("New folder", "Enter folder name:", "New folder", parentCollection); + var newFolderName = PromptUniqueFolderName("New folder", "Enter folder name:", "New folder", parentCollection); if (newFolderName == null) return; - string fullPath = string.IsNullOrEmpty(parentFolder) ? newFolderName : parentFolder + _folderSeparator + newFolderName; + var fullPath = string.IsNullOrEmpty(parentFolder) ? newFolderName : parentFolder + _folderSeparator + newFolderName; GetOrCreateFolderNodes(fullPath); var newNode = treeEvents.GetAllNodes().LastOrDefault(n => IsFolderNode(n) && n.Text == newFolderName); @@ -910,7 +909,7 @@ private void RenameFolder(DarkTreeNode node) return; var siblings = node.ParentNode?.Nodes ?? treeEvents.Nodes; - string newName = PromptUniqueFolderName("Rename folder", "Enter new folder name:", node.Text, siblings, node); + var newName = PromptUniqueFolderName("Rename folder", "Enter new folder name:", node.Text, siblings, node); if (newName == null || newName == node.Text) return; @@ -922,7 +921,7 @@ private void RenameFolder(DarkTreeNode node) // an existing folder in the given collection, or cancels. Returns null on cancel or empty input. private string PromptUniqueFolderName(string title, string prompt, string initialValue, ObservableList collection, DarkTreeNode exclude = null) { - string current = initialValue; + var current = initialValue; while (true) { using (var inputBox = new FormInputBox(title, prompt, current)) @@ -930,7 +929,7 @@ private string PromptUniqueFolderName(string title, string prompt, string initia if (inputBox.ShowDialog(this) != DialogResult.OK) return null; - string name = inputBox.Result.Trim(); + var name = inputBox.Result.Trim(); if (string.IsNullOrEmpty(name)) return null; diff --git a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs index a84ab45ad3..d6917769ce 100644 --- a/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs +++ b/TombLib/TombLib.Forms/Controls/VisualScripting/NodeEditor.cs @@ -935,7 +935,7 @@ private void DrawHeader(PaintEventArgs e, VisibleNodeBase node) var textRect = new Rectangle(rect.X + iconOffset, rect.Y, rect.Width - iconOffset, rect.Height); using (var b = new SolidBrush(Colors.LightText.ToFloat3Color().ToWinFormsColor(LabelOpacity))) e.Graphics.DrawString(headerText, Font, b, textRect, - new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center }); + new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Center }); var condNode = node as VisibleNodeCondition; if (condNode == null)