using System; using System.IO; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization.Metadata; using System.Windows.Forms; namespace Better_NCP_Editor { public partial class Form1 : Form { private string currentJsonFilePath; private JsonNode currentJson; private bool fileModified = false; private ToolTips toolTips = new ToolTips(); // In your Form1 class: private ToolTip customToolTip = new ToolTip(); private TreeNode lastHoveredNode = null; public Form1() { InitializeComponent(); dirTreeView.AfterSelect += DirTreeView_AfterSelect; entityTreeView.NodeMouseDoubleClick += entityTreeView_NodeMouseDoubleClick; entityTreeView.AfterSelect += entityTreeView_AfterSelect; // Prevent the form from shrinking below its current size. this.MinimumSize = new Size(1045, 700); // Set minimum allowed size this.FormBorderStyle = FormBorderStyle.Sizable; // Example layout using docking: dirTreeView.Dock = DockStyle.Left; entityTreeView.Dock = DockStyle.Fill; // Optionally, set a fixed width for the dirTreeView: dirTreeView.Width = 290; // In the designer or constructor: dirTreeView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left; entityTreeView.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; entityTreeView.ShowNodeToolTips = false; // Configure the custom tooltip. customToolTip.AutoPopDelay = 15000; // Show for 15 seconds. customToolTip.AutomaticDelay = 1000; customToolTip.ShowAlways = true; customToolTip.InitialDelay = 500; customToolTip.ReshowDelay = 500; customToolTip.UseAnimation = true; // Subscribe to events. entityTreeView.MouseMove += EntityTreeView_MouseMove; entityTreeView.MouseLeave += EntityTreeView_MouseLeave; } private void EntityTreeView_MouseMove(object sender, MouseEventArgs e) { int xOffset = 15; int yOffset = 0; TreeNode node = entityTreeView.GetNodeAt(e.Location); if (node != null) { // Measure the text size for the node using the TreeView's font. Size textSize = TextRenderer.MeasureText(node.Text, entityTreeView.Font); // Construct a rectangle that represents the area occupied by the text. Rectangle textRect = new Rectangle(node.Bounds.X, node.Bounds.Y, textSize.Width, node.Bounds.Height); if (textRect.Contains(e.Location)) { if (node != lastHoveredNode) { lastHoveredNode = node; // Use an offset so the tooltip doesn't overlap the text. Point offsetLocation = new Point(e.Location.X + xOffset, e.Location.Y + xOffset); customToolTip.Show(node.ToolTipText, entityTreeView, offsetLocation, customToolTip.AutoPopDelay); } } else { customToolTip.Hide(entityTreeView); lastHoveredNode = null; } } else { customToolTip.Hide(entityTreeView); lastHoveredNode = null; } } private void EntityTreeView_MouseLeave(object sender, EventArgs e) { customToolTip.Hide(entityTreeView); lastHoveredNode = null; } private void Form1_Load(object sender, EventArgs e) { // Other initialization code, if needed. } private void btn_load_Click(object sender, EventArgs e) { using FolderBrowserDialog folderDialog = new(); folderDialog.Description = "Select a directory containing JSON files"; folderDialog.UseDescriptionForTitle = true; folderDialog.ShowNewFolderButton = false; if (folderDialog.ShowDialog() == DialogResult.OK) { string selectedPath = folderDialog.SelectedPath; dirTreeView.Nodes.Clear(); try { TreeNode rootNode = new TreeNode(Path.GetFileName(selectedPath)) { Tag = selectedPath }; dirTreeView.Nodes.Add(rootNode); LoadJsonFiles(rootNode, selectedPath); rootNode.Expand(); statusTextbox.Text = $"Loaded directory: {selectedPath}"; } catch (Exception ex) { MessageBox.Show($"Error loading directory: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void LoadJsonFiles(TreeNode parentNode, string directoryPath) { foreach (string filePath in Directory.GetFiles(directoryPath, "*.json")) { TreeNode fileNode = new TreeNode(Path.GetFileName(filePath)) { Tag = filePath }; parentNode.Nodes.Add(fileNode); } foreach (string subDirPath in Directory.GetDirectories(directoryPath)) { TreeNode subDirNode = new TreeNode(Path.GetFileName(subDirPath)) { Tag = subDirPath }; parentNode.Nodes.Add(subDirNode); LoadJsonFiles(subDirNode, subDirPath); } } private void DirTreeView_AfterSelect(object sender, TreeViewEventArgs e) { if (e.Node?.Tag is string filePath && Path.GetExtension(filePath).Equals(".json", StringComparison.OrdinalIgnoreCase)) { LoadJsonFile(filePath); } } private void LoadJsonFile(string jsonFilePath) { try { currentJsonFilePath = jsonFilePath; string json = File.ReadAllText(jsonFilePath); currentJson = JsonNode.Parse(json); PopulateEntityTree(currentJson); fileModified = false; btn_save.Enabled = false; // No changes yet statusTextbox.Text = $"Loaded JSON file: {jsonFilePath}"; } catch (Exception ex) { MessageBox.Show($"Error loading JSON file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } // Populates the entityTreeView recursively from a JsonNode. private void PopulateEntityTree(JsonNode json) { entityTreeView.Nodes.Clear(); // Use the filename of the current JSON file if available; otherwise default to "JSON". string fileName = string.IsNullOrEmpty(currentJsonFilePath) ? "JSON" : Path.GetFileName(currentJsonFilePath); TreeNode root = new TreeNode(fileName) { Tag = json }; entityTreeView.Nodes.Add(root); PopulateTreeRecursive(json, root); root.ExpandAll(); entityTreeView.TopNode = root; } private void PopulateTreeRecursive(JsonNode node, TreeNode treeNode) { if (node is JsonObject obj) { foreach (var kvp in obj) { TreeNode child; if (kvp.Value is JsonValue) { child = new TreeNode($"{kvp.Key}: {kvp.Value}"); } else { child = new TreeNode(kvp.Key); } child.ToolTipText = toolTips.tips.ContainsKey(kvp.Key) ? toolTips.tips[kvp.Key] : ""; child.Tag = kvp.Value; treeNode.Nodes.Add(child); PopulateTreeRecursive(kvp.Value, child); } } else if (node is JsonArray arr) { for (int i = 0; i < arr.Count; i++) { JsonNode item = arr[i]; TreeNode child; if (item is JsonValue) { child = new TreeNode($"[{i}]: {item}"); } else { child = new TreeNode($"[{i}]"); } child.Tag = item; treeNode.Nodes.Add(child); PopulateTreeRecursive(item, child); } } } private void entityTreeView_AfterSelect(object sender, TreeViewEventArgs e) { if (e.Node != null) { bool enableAddDelButtons = false; bool enableImportExportButtons = false; // Enable if the selected node's Tag is a JsonArray if (e.Node.Tag is JsonArray) { //enableButtons = true; enableImportExportButtons = true; } // Or if the selected node is an item in an array (its parent is a JsonArray) else if (e.Node.Parent != null && e.Node.Parent.Tag is JsonArray) { enableAddDelButtons = true; enableImportExportButtons = true; } // You can also add additional conditions for a "preset" node, e.g. by checking text: // else if (e.Node.Text.StartsWith("Preset", StringComparison.OrdinalIgnoreCase)) // { // enableButtons = true; // } btn_entity_add.Enabled = enableAddDelButtons; btn_entity_del.Enabled = enableAddDelButtons; btn_export_entityData.Enabled = enableImportExportButtons; btn_import_entityData.Enabled = enableImportExportButtons; } else { btn_entity_add.Enabled = false; btn_entity_del.Enabled = false; btn_export_entityData.Enabled = false; btn_import_entityData.Enabled = false; } } // When a tree node is double-clicked, open an edit window for that value. private void entityTreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { if (e.Node == null) return; // Expect node text in the form "Property: Value" string[] parts = e.Node.Text.Split(new[] { ':' }, 2); if (parts.Length != 2) return; string propName = parts[0].Trim(); string currentVal = parts[1].Trim(); // Use simple heuristics to determine the type. Type valueType = typeof(string); if (bool.TryParse(currentVal, out bool b)) valueType = typeof(bool); else if (int.TryParse(currentVal, out int i)) valueType = typeof(int); using (EditValueForm editForm = new EditValueForm(propName, currentVal, valueType)) { if (editForm.ShowDialog() == DialogResult.OK) { object newVal = editForm.NewValue; string displayVal = newVal is bool ? newVal.ToString().ToLower() : newVal.ToString(); e.Node.Text = $"{propName}: {displayVal}"; if (e.Node.Tag is JsonNode node) { JsonNode newNode = JsonValue.Create(newVal); node.ReplaceWith(newNode); e.Node.Tag = newNode; } // Mark the file as modified. fileModified = true; btn_save.Enabled = true; } } } private void btn_save_Click(object sender, EventArgs e) { if (string.IsNullOrEmpty(currentJsonFilePath) || currentJson == null) { MessageBox.Show("No JSON file loaded.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } try { var options = new JsonSerializerOptions { WriteIndented = true, TypeInfoResolver = new DefaultJsonTypeInfoResolver(), Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; string newJson = currentJson.ToJsonString(options); File.WriteAllText(currentJsonFilePath, newJson); statusTextbox.Text = "JSON file saved successfully."; //MessageBox.Show("JSON file saved successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); // Reset the modified flag. fileModified = false; btn_save.Enabled = false; } catch (Exception ex) { MessageBox.Show($"Error saving JSON file: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private TreeNode FindNodeByFullPath(TreeNodeCollection nodes, string fullPath) { foreach (TreeNode node in nodes) { if (node.FullPath == fullPath) return node; TreeNode found = FindNodeByFullPath(node.Nodes, fullPath); if (found != null) return found; } return null; } private void btn_entity_add_Click(object sender, EventArgs e) { TreeNode selected = entityTreeView.SelectedNode; if (selected == null) { MessageBox.Show("Please select a node to duplicate.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } TreeNode parent = selected.Parent; if (parent == null) { MessageBox.Show("The selected node has no parent (cannot duplicate).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Ensure the parent's Tag is a JsonArray. if (!(parent.Tag is JsonArray parentArray)) { MessageBox.Show("The selected node's parent is not an array node.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Retrieve the underlying JsonNode for the selected node. if (!(selected.Tag is JsonNode selectedJsonNode)) { MessageBox.Show("Selected node does not contain a valid JSON element.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Duplicate (deep clone) the selected JsonNode. JsonNode duplicate = selectedJsonNode.DeepClone(); // Add the duplicate to the parent's array. parentArray.Add(duplicate); // Refresh only the parent's children. parent.Nodes.Clear(); PopulateTreeRecursive((JsonNode)parent.Tag, parent); parent.ExpandAll(); // Expand the parent and all its child nodes fileModified = true; btn_save.Enabled = true; // Select the newly added item (last child in the parent's node collection). if (parent.Nodes.Count > 0) { TreeNode newSelected = parent.Nodes[parent.Nodes.Count - 1]; entityTreeView.SelectedNode = newSelected; newSelected.EnsureVisible(); entityTreeView.Focus(); } } private void btn_entity_del_Click(object sender, EventArgs e) { TreeNode selected = entityTreeView.SelectedNode; if (selected == null) { MessageBox.Show("Please select a node to delete.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } TreeNode parent = selected.Parent; if (parent == null) { MessageBox.Show("The selected node has no parent (cannot delete).", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Ensure the parent's Tag is a JsonArray. if (!(parent.Tag is JsonArray parentArray)) { MessageBox.Show("The selected node's parent is not an array node.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Determine the index of the selected node (assumed to be in the same order as in the array). int indexToRemove = selected.Index; if (indexToRemove < 0 || indexToRemove >= parentArray.Count) { MessageBox.Show("Selected node index is invalid.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // If this is the last item in the array, ask for confirmation. if (parentArray.Count == 1) { DialogResult confirm = MessageBox.Show("Are you sure you want to delete the last item in the array?", "Confirm Deletion", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (confirm != DialogResult.Yes) { return; } } // Remove the element from the underlying JsonArray. parentArray.RemoveAt(indexToRemove); // Refresh only the parent's children. parent.Nodes.Clear(); PopulateTreeRecursive((JsonNode)parent.Tag, parent); parent.ExpandAll(); fileModified = true; btn_save.Enabled = true; // Select the newly last item in the array; if none, select the parent. if (parent.Nodes.Count > 0) { TreeNode newSelected = parent.Nodes[parent.Nodes.Count - 1]; entityTreeView.SelectedNode = newSelected; newSelected.EnsureVisible(); entityTreeView.Focus(); } else { entityTreeView.SelectedNode = parent; parent.EnsureVisible(); entityTreeView.Focus(); } } // Compare two JsonNode structures (objects, arrays, or values) for basic structural compatibility. private bool CompareJsonStructure(JsonNode a, JsonNode b) { if (a == null || b == null) return a == b; // Ensure both nodes are of the same concrete type. if (a.GetType() != b.GetType()) return false; if (a is JsonObject objA && b is JsonObject objB) { // They must have the same set of keys. if (objA.Count != objB.Count) return false; foreach (var kvp in objA) { if (!objB.ContainsKey(kvp.Key)) return false; if (!CompareJsonStructure(kvp.Value, objB[kvp.Key])) return false; } return true; } else if (a is JsonArray arrA && b is JsonArray arrB) { // If both arrays are empty, we consider them structurally compatible. if (arrA.Count == 0 && arrB.Count == 0) return true; // Otherwise, compare the structure of their first elements. if (arrA.Count > 0 && arrB.Count > 0) return CompareJsonStructure(arrA[0], arrB[0]); return false; } else if (a is JsonValue && b is JsonValue) { try { // Compare the underlying value types. object valA = a.GetValue(); object valB = b.GetValue(); return valA?.GetType() == valB?.GetType(); } catch { return false; } } return false; } private void btn_import_entityData_Click(object sender, EventArgs e) { // Ensure a node is selected. TreeNode selected = entityTreeView.SelectedNode; if (selected == null) { MessageBox.Show("Please select a node for importing data.", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } // Open a file picker to select the import file. using (OpenFileDialog openDialog = new OpenFileDialog()) { openDialog.Filter = "JSON Files (*.json)|*.json|All Files (*.*)|*.*"; openDialog.Title = "Import JSON Data"; if (openDialog.ShowDialog() == DialogResult.OK) { try { string importText = File.ReadAllText(openDialog.FileName); JsonNode importJson = JsonNode.Parse(importText); // Determine if we are adding to an array or replacing a child. // If the selected node's Tag is a JsonArray, we add to it. // If the selected node's parent is a JsonArray, we replace the selected node. JsonArray parentArray = null; bool isAddingNewItem = false; if (selected.Tag is JsonArray) { parentArray = (JsonArray)selected.Tag; isAddingNewItem = true; } else if (selected.Parent != null && selected.Parent.Tag is JsonArray) { parentArray = (JsonArray)selected.Parent.Tag; isAddingNewItem = false; } else { MessageBox.Show("The selected node is not part of an array. Import operation cancelled.", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } // Determine the target node for structure comparison. // If adding a new item, compare the structure of the array's first item (if any). // Otherwise, compare the selected node's structure. JsonNode targetStructure = null; if (isAddingNewItem) { if (parentArray.Count > 0) targetStructure = parentArray[0]; else { // If the array is empty, we assume it's acceptable. targetStructure = importJson; } } else { targetStructure = (JsonNode)selected.Tag; } if (!CompareJsonStructure(importJson, targetStructure)) { MessageBox.Show("The imported JSON structure does not match the target structure.", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } fileModified = true; btn_save.Enabled = true; if (isAddingNewItem) { // Add the imported JSON as a new item to the array. parentArray.Add(importJson); // Find the TreeNode corresponding to the array (selected node). TreeNode arrayNode = selected; // Refresh only this node's children. arrayNode.Nodes.Clear(); PopulateTreeRecursive((JsonNode)arrayNode.Tag, arrayNode); arrayNode.ExpandAll(); // Select the newly added item (last child). if (arrayNode.Nodes.Count > 0) { TreeNode newNode = arrayNode.Nodes[arrayNode.Nodes.Count - 1]; entityTreeView.SelectedNode = newNode; newNode.EnsureVisible(); entityTreeView.Focus(); } } else { // Replace the selected node's data with the imported JSON. JsonNode oldNode = (JsonNode)selected.Tag; oldNode.ReplaceWith(importJson); selected.Tag = importJson; // Refresh the parent node. TreeNode parentNode = selected.Parent; parentNode.Nodes.Clear(); PopulateTreeRecursive((JsonNode)parentNode.Tag, parentNode); parentNode.ExpandAll(); // Try to reselect the replaced item (by its index). int index = selected.Index; if (index >= 0 && index < parentNode.Nodes.Count) { TreeNode newNode = parentNode.Nodes[index]; entityTreeView.SelectedNode = newNode; newNode.EnsureVisible(); entityTreeView.Focus(); } } } catch (Exception ex) { MessageBox.Show($"Error importing JSON file: {ex.Message}", "Import Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } private void btn_export_entityData_Click(object sender, EventArgs e) { // Ensure a node is selected and that it contains a JsonNode. if (entityTreeView.SelectedNode == null || !(entityTreeView.SelectedNode.Tag is JsonNode selectedJson)) { MessageBox.Show("Please select a node with valid JSON data to export.", "Export Error", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } using (SaveFileDialog saveDialog = new SaveFileDialog()) { saveDialog.Filter = "JSON Files (*.json)|*.json|All Files (*.*)|*.*"; saveDialog.Title = "Export JSON Data"; saveDialog.FileName = "exported.json"; if (saveDialog.ShowDialog() == DialogResult.OK) { try { var options = new JsonSerializerOptions { WriteIndented = true, TypeInfoResolver = new DefaultJsonTypeInfoResolver(), Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; // Export the JSON subtree starting at the selected node. string exportJson = selectedJson.ToJsonString(options); File.WriteAllText(saveDialog.FileName, exportJson); MessageBox.Show("Export successful!", "Export", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"Error exporting JSON: {ex.Message}", "Export Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } } }