UltimateZoneBuilder/Source/Core/Controls/Scripting/ScriptResourcesControl.cs
MaxED 88363a1a66 Added, Script Editor: added "Find usages" option (available in the "Search" menu, via text editor context menu, via Ctrl-Shift-F shortcut and in the "Find and Replace" window).
Added, Script Editor: double-clicking text resource tab header now shows the corresponding resource in the Resources tree.
Updated, Game configurations, UDMF: added several missing Thing renderstyles.
2016-12-08 12:10:43 +00:00

544 lines
18 KiB
C#

#region ================== Namespaces
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Config;
using CodeImp.DoomBuilder.Data;
using CodeImp.DoomBuilder.Data.Scripting;
using CodeImp.DoomBuilder.Windows;
#endregion
namespace CodeImp.DoomBuilder.Controls
{
internal partial class ScriptResourcesControl : UserControl
{
#region ================== Nodes sorter
private class ScriptNodesSorter : IComparer
{
private List<string> resourceorder;
private HashSet<TextResourceNodeType> resourcetypes;
public ScriptNodesSorter()
{
// Needed to show resources in the map-defined order
resourceorder = new List<string>();
foreach(DataReader reader in General.Map.Data.Containers)
{
resourceorder.Add(reader.Location.location);
}
resourcetypes = new HashSet<TextResourceNodeType> { TextResourceNodeType.RESOURCE_DIRECTORY, TextResourceNodeType.RESOURCE_PK3, TextResourceNodeType.RESOURCE_WAD };
}
// Compare between two tree nodes
public int Compare(object o1, object o2)
{
TreeNode n1 = o1 as TreeNode;
TreeNode n2 = o2 as TreeNode;
TextResourceNodeData d1 = (TextResourceNodeData)n1.Tag;
TextResourceNodeData d2 = (TextResourceNodeData)n2.Tag;
// Sort resources by load order
if(resourcetypes.Contains(d1.NodeType) && resourcetypes.Contains(d2.NodeType))
{
int n1index = resourceorder.IndexOf(d1.ResourceLocation);
int n2index = resourceorder.IndexOf(d2.ResourceLocation);
if(n1index > n2index) return 1;
if(n1index < n2index) return -1;
return 0;
}
// Push map namespace resources before anything else
if(d1.NodeType == TextResourceNodeType.RESOURCE_MAP && d2.NodeType != TextResourceNodeType.RESOURCE_MAP) return -1;
if(d1.NodeType != TextResourceNodeType.RESOURCE_MAP && d2.NodeType == TextResourceNodeType.RESOURCE_MAP) return 1;
// Push embedded WADs before anything else except map resources
if(n1.Parent != null && n2.Parent != null)
{
if(d1.NodeType == TextResourceNodeType.RESOURCE_WAD && d2.NodeType != TextResourceNodeType.RESOURCE_WAD) return -1;
if(d1.NodeType != TextResourceNodeType.RESOURCE_WAD && d2.NodeType == TextResourceNodeType.RESOURCE_WAD) return 1;
}
// Push script folders before script files
if(d1.NodeType == TextResourceNodeType.DIRECTORY && d2.NodeType != TextResourceNodeType.DIRECTORY) return -1;
if(d1.NodeType != TextResourceNodeType.DIRECTORY && d2.NodeType == TextResourceNodeType.DIRECTORY) return 1;
// Sort by name
return n1.Text.CompareTo(n2.Text);
}
}
#endregion
#region ================== Constants
#endregion
#region ================== Enums
private enum TextResourceNodeType
{
RESOURCE_WAD,
RESOURCE_DIRECTORY,
RESOURCE_PK3,
RESOURCE_MAP,
DIRECTORY,
NODE,
}
#endregion
#region ================== Structs
private struct TextResourceNodeData
{
public ScriptResource Resource;
public string ResourceLocation; // Where PK3/WAD/Folder resource is located
public string LocationInResource; // Path to text file inside resource
public TextResourceNodeType NodeType;
public ScriptType ScriptType;
public override string ToString()
{
return (NodeType == TextResourceNodeType.NODE
? Path.Combine(ResourceLocation, LocationInResource) + (Resource.LumpIndex != -1 ? ":" + Resource.LumpIndex : "")
: ResourceLocation);
}
}
private struct ScriptTypeItem
{
public string Name;
public ScriptType Type;
public override string ToString() { return Name; }
}
#endregion
#region ================== Variables
private ScriptEditorPanel scriptpanel;
private Dictionary<string, Dictionary<ScriptType, HashSet<ScriptResource>>> resourcesperlocation;
private List<ScriptTypeItem> usedscripttypes;
#endregion
#region ================== Properties
#endregion
#region ================== Constructor / Setup
public ScriptResourcesControl()
{
InitializeComponent();
}
public void Setup(ScriptEditorPanel scriptpanel, Dictionary<ScriptType, HashSet<ScriptResource>> resources)
{
this.scriptpanel = scriptpanel;
projecttree.ImageList = scriptpanel.Icons.Icons; // Link icons
resourcesperlocation = new Dictionary<string, Dictionary<ScriptType, HashSet<ScriptResource>>>();
foreach(HashSet<ScriptResource> group in resources.Values)
{
foreach(ScriptResource resource in group)
{
string key = resource.Resource.Location.location;
if(!resourcesperlocation.ContainsKey(key))
resourcesperlocation.Add(key, new Dictionary<ScriptType, HashSet<ScriptResource>>());
if(!resourcesperlocation[key].ContainsKey(resource.ScriptType))
resourcesperlocation[key].Add(resource.ScriptType, new HashSet<ScriptResource>());
resourcesperlocation[key][resource.ScriptType].Add(resource);
}
}
// Add used script types to the filter combobox
string curfilter = filterbytype.SelectedText;
filterbytype.Items.Clear();
filterbytype.Items.Add(new ScriptTypeItem { Name = "All", Type = ScriptType.UNKNOWN });
usedscripttypes = new List<ScriptTypeItem>();
foreach(ScriptType st in resources.Keys)
usedscripttypes.Add(new ScriptTypeItem { Name = Enum.GetName(typeof(ScriptType), st), Type = st });
usedscripttypes.Sort((i1, i2) => String.Compare(i1.Name, i2.Name, StringComparison.Ordinal));
int toselect = 0;
for(int i = 0; i < usedscripttypes.Count; i++)
{
if(usedscripttypes[i].Name == curfilter) toselect = i;
filterbytype.Items.Add(usedscripttypes[i]);
}
filterbytype.SelectedIndex = toselect; // This will also trigger tree update
}
#endregion
#region ================== Methods
public void SelectItem(ScriptResourceDocumentTab tab)
{
if(tab == null) return;
SelectItem(tab.Resource.Resource.Location.location, tab.Resource.Filename,
tab.Resource.LumpIndex, tab.Resource.ScriptType);
}
public void SelectItem(string resourcelocation, string lumpname, int lumpindex, ScriptType scripttype)
{
TreeNode target = FindItem(projecttree.Nodes, resourcelocation, lumpname, lumpindex, scripttype);
if(target != null)
{
projecttree.SelectedNode = target;
}
}
private static TreeNode FindItem(TreeNodeCollection nodes, string resourcelocation, string lumpname, int lumpindex, ScriptType scripttype)
{
foreach(TreeNode node in nodes)
{
// Is this the item we are looking for?
TextResourceNodeData data = (TextResourceNodeData)node.Tag;
if(data.NodeType == TextResourceNodeType.NODE && data.ResourceLocation == resourcelocation
&& data.ScriptType == scripttype && data.Resource.Filename == lumpname && data.Resource.LumpIndex == lumpindex)
{
// Found it!
return node;
}
// Try children...
if(node.Nodes.Count > 0)
{
TreeNode item = FindItem(node.Nodes, resourcelocation, lumpname, lumpindex, scripttype);
if(item != null) return item;
}
}
// No dice...
return null;
}
private void UpdateResourcesTree()
{
ScriptType targettype = (filterbytype.SelectedIndex > -1 ? ((ScriptTypeItem)filterbytype.SelectedItem).Type : ScriptType.UNKNOWN);
UpdateResourcesTree(targettype, filterproject.Text);
}
private void UpdateResourcesTree(ScriptType filtertype, string filterfilename)
{
TreeNode selected = projecttree.SelectedNode;
TreeNode toselect = null;
projecttree.BeginUpdate();
projecttree.Nodes.Clear();
char[] splitter = { Path.DirectorySeparatorChar };
bool filenamefiltered = !string.IsNullOrEmpty(filterfilename);
bool filteringapplied = (filenamefiltered || filtertype != ScriptType.UNKNOWN);
// Create nodes
foreach(KeyValuePair<string, Dictionary<ScriptType, HashSet<ScriptResource>>> group in resourcesperlocation)
{
foreach(ScriptTypeItem item in usedscripttypes)
{
// Filter by script type?
if(filtertype != ScriptType.UNKNOWN && item.Type != filtertype) continue;
// Current resource has this scrit type?
if(!group.Value.ContainsKey(item.Type)) continue;
HashSet<ScriptResource> resources = group.Value[item.Type];
foreach(ScriptResource res in resources)
{
bool asreadonly = res.Resource.IsReadOnly;
// Filter by filename?
if(filenamefiltered && Path.GetFileName(res.Filename).IndexOf(filterfilename, StringComparison.OrdinalIgnoreCase) == -1)
continue;
// Resource type node added?
TreeNode root;
string key = res.Resource.Location.location;
// WAD resource inside another resource?
if(res.Resource is WADReader && ((WADReader)res.Resource).ParentResource != null)
{
WADReader wr = (WADReader)res.Resource;
string parentkey = wr.ParentResource.Location.location;
TreeNode parent = GetResourceNode(projecttree.Nodes, wr.ParentResource.Location.GetDisplayName(), parentkey, wr.ParentResource);
if(parent.Nodes.ContainsKey(key))
{
root = parent.Nodes[key];
}
else
{
root = GetResourceNode(parent.Nodes, Path.GetFileName(wr.Location.GetDisplayName()), key, res.Resource);
TrySelectNode(selected, root, ref toselect);
}
}
else if(projecttree.Nodes.ContainsKey(key))
{
root = projecttree.Nodes[key];
}
else
{
root = GetResourceNode(projecttree.Nodes, res.Resource.Location.GetDisplayName(), key, res.Resource);
TrySelectNode(selected, root, ref toselect);
}
// Single resource item or active filtering?
int iconindex = scriptpanel.Icons.GetScriptIcon(res.ScriptType);
if(filteringapplied || (resources.Count == 1 && res.ScriptType != ScriptType.ACS))
{
// Create new node
var data = new TextResourceNodeData
{
ResourceLocation = key,
LocationInResource = Path.GetDirectoryName(res.Filename),
NodeType = TextResourceNodeType.NODE,
Resource = res,
ScriptType = res.ScriptType,
};
TreeNode scriptnode = new TreeNode(res.ToString(), iconindex, iconindex) { Tag = data, ToolTipText = data.ToString() };
if(asreadonly) scriptnode.ForeColor = SystemColors.GrayText;
TrySelectNode(selected, scriptnode, ref toselect);
// Add the node
root.Nodes.Add(scriptnode);
}
else
{
// Script type added?
string typename = "[" + Enum.GetName(typeof(ScriptType), res.ScriptType) + "]";
int groupiconindex = scriptpanel.Icons.GetScriptFolderIcon(res.ScriptType, false);
TreeNode scriptroot;
if(root.Nodes.ContainsKey(typename))
{
scriptroot = root.Nodes[typename];
}
else
{
var rdata = new TextResourceNodeData
{
ResourceLocation = key,
NodeType = TextResourceNodeType.DIRECTORY,
ScriptType = res.ScriptType,
};
scriptroot = new TreeNode(typename, groupiconindex, groupiconindex)
{
Tag = rdata,
Name = typename,
ToolTipText = rdata.ToString()
};
if(asreadonly) scriptroot.ForeColor = SystemColors.GrayText;
root.Nodes.Add(scriptroot);
TrySelectNode(selected, scriptroot, ref toselect);
}
// Add the resource path nodes if needed...
string path = Path.GetDirectoryName(res.Filename);
TreeNode pathnode = scriptroot;
string localpath = string.Empty;
if(!string.IsNullOrEmpty(path))
{
List<string> parts = new List<string>(path.Split(splitter, StringSplitOptions.RemoveEmptyEntries));
while(parts.Count > 0)
{
if(pathnode.Nodes.ContainsKey(parts[0]))
{
pathnode = pathnode.Nodes[parts[0]];
}
else
{
localpath = Path.Combine(localpath, parts[0]);
TreeNode child = new TreeNode(parts[0], groupiconindex, groupiconindex);
child.Name = parts[0];
var cdata = new TextResourceNodeData
{
ResourceLocation = key,
LocationInResource = path,
NodeType = TextResourceNodeType.DIRECTORY,
ScriptType = res.ScriptType
};
child.Tag = cdata;
child.ToolTipText = cdata.ToString();
if(asreadonly) child.ForeColor = SystemColors.GrayText;
pathnode.Nodes.Add(child);
pathnode = child;
TrySelectNode(selected, pathnode, ref toselect);
}
parts.RemoveAt(0);
}
}
// Create new node
TextResourceNodeData data = new TextResourceNodeData
{
ResourceLocation = key,
LocationInResource = path,
NodeType = TextResourceNodeType.NODE,
Resource = res,
ScriptType = res.ScriptType,
};
string includepath = (res.ScriptType == ScriptType.ACS ? "\nInclude path: \"" + res.Filename + "\"" : "");
TreeNode scriptnode = new TreeNode(res.ToString(), iconindex, iconindex) { Tag = data, ToolTipText = data + includepath };
if(asreadonly) scriptnode.ForeColor = SystemColors.GrayText;
TrySelectNode(selected, scriptnode, ref toselect);
// Add the node
pathnode.Nodes.Add(scriptnode);
}
}
}
}
// If there's only one root node, shift all nodes up
if(projecttree.Nodes.Count == 1 && projecttree.Nodes[0].Nodes.Count > 0)
{
TreeNode[] children = new TreeNode[projecttree.Nodes[0].Nodes.Count];
projecttree.Nodes[0].Nodes.CopyTo(children, 0);
projecttree.Nodes.Clear();
projecttree.Nodes.AddRange(children);
}
// Sort the nodes
projecttree.TreeViewNodeSorter = new ScriptNodesSorter();
// Have valid selection?
if(toselect != null) projecttree.SelectedNode = toselect;
// Expand all nodes when filtered
if(filenamefiltered) projecttree.ExpandAll();
projecttree.EndUpdate();
}
private TreeNode GetResourceNode(TreeNodeCollection nodes, string title, string key, DataReader resource)
{
// Node already added?
if(nodes.ContainsKey(key)) return nodes[key];
// Create new node
int resourceiconindex = scriptpanel.Icons.GetResourceIcon(resource.Location.type);
TreeNode root = new TreeNode(title, resourceiconindex, resourceiconindex);
root.Name = key;
root.Tag = new TextResourceNodeData { ResourceLocation = key, NodeType = (TextResourceNodeType)resource.Location.type };
root.ToolTipText = key;
if(resource.IsReadOnly) root.ForeColor = SystemColors.GrayText;
nodes.Add(root);
return root;
}
private static void TrySelectNode(TreeNode oldselection, TreeNode node, ref TreeNode toselect)
{
if(oldselection == null || oldselection.Text != node.Text
|| oldselection.ToolTipText != node.ToolTipText
|| oldselection.Tag.ToString() != node.Tag.ToString())
return;
// Found match!
toselect = node;
}
protected override void OnLoad(EventArgs e)
{
// Manual reposition required...
if(MainForm.DPIScaler.Width != 1.0f || MainForm.DPIScaler.Height != 1.0f)
{
filterprojectclear.Left = this.Width - filterprojectclear.Width - filterprojectclear.Margin.Right;
filterproject.Width = filterprojectclear.Left - filterprojectclear.Margin.Left - filterproject.Left;
filterbytype.Left = filterproject.Left;
filterbytype.Width = filterprojectclear.Right - filterproject.Left;
projecttree.Width = this.Width - projecttree.Left - projecttree.Margin.Right;
projecttree.Height = this.Height - projecttree.Top - projecttree.Margin.Bottom;
}
base.OnLoad(e);
}
#endregion
#region ================== Events
private void filterproject_TextChanged(object sender, EventArgs e)
{
UpdateResourcesTree();
}
private void filterprojectclear_Click(object sender, EventArgs e)
{
filterproject.Clear();
}
private void filterbytype_SelectedIndexChanged(object sender, EventArgs e)
{
UpdateResourcesTree();
}
private void projecttree_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
// Select node on Right-click
projecttree.SelectedNode = e.Node;
}
private void projecttree_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
{
//TODO: special handling for SCRIPTS/DIALOGUE
if(!(e.Node.Tag is TextResourceNodeData)) throw new NotSupportedException("Tag must be TextResourceData!");
TextResourceNodeData data = (TextResourceNodeData)e.Node.Tag;
// Open file
if(data.Resource != null) scriptpanel.OpenResource(data.Resource);
}
// Switch to opened resource icon
private void projecttree_BeforeExpand(object sender, TreeViewCancelEventArgs e)
{
if(!(e.Node.Tag is TextResourceNodeData)) throw new NotSupportedException("Tag must be TextResourceData!");
TextResourceNodeData data = (TextResourceNodeData)e.Node.Tag;
// Group node?
if(data.NodeType == TextResourceNodeType.DIRECTORY)
{
e.Node.ImageIndex = scriptpanel.Icons.GetScriptFolderIcon(data.ScriptType, true);
e.Node.SelectedImageIndex = e.Node.ImageIndex;
}
}
// Switch to closed resource icon
private void projecttree_BeforeCollapse(object sender, TreeViewCancelEventArgs e)
{
if(!(e.Node.Tag is TextResourceNodeData)) throw new NotSupportedException("Tag must be TextResourceData!");
TextResourceNodeData data = (TextResourceNodeData)e.Node.Tag;
// Group node?
if(data.NodeType == TextResourceNodeType.DIRECTORY)
{
e.Node.ImageIndex = scriptpanel.Icons.GetScriptFolderIcon(data.ScriptType, false);
e.Node.SelectedImageIndex = e.Node.ImageIndex;
}
}
#endregion
}
}