#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 resourceorder; private HashSet resourcetypes; public ScriptNodesSorter() { // Needed to show resources in the map-defined order resourceorder = new List(); foreach(DataReader reader in General.Map.Data.Containers) { resourceorder.Add(reader.Location.location); } resourcetypes = new HashSet { 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>> resourcesperlocation; private List usedscripttypes; #endregion #region ================== Properties #endregion #region ================== Constructor / Setup public ScriptResourcesControl() { InitializeComponent(); } public void Setup(ScriptEditorPanel scriptpanel, Dictionary> resources) { this.scriptpanel = scriptpanel; projecttree.ImageList = scriptpanel.Icons.Icons; // Link icons resourcesperlocation = new Dictionary>>(); foreach(HashSet group in resources.Values) { foreach(ScriptResource resource in group) { string key = resource.Resource.Location.location; if(!resourcesperlocation.ContainsKey(key)) resourcesperlocation.Add(key, new Dictionary>()); if(!resourcesperlocation[key].ContainsKey(resource.ScriptType)) resourcesperlocation[key].Add(resource.ScriptType, new HashSet()); 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(); 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>> 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 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 parts = new List(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 } }