2025-03-04 20:20:08 -08:00

717 lines
29 KiB
C#

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>();
object valB = b.GetValue<object>();
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);
}
}
}
}
}
}