#region ================== Copyright (c) 2007 Pascal vd Heiden /* * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com * This program is released under GNU General Public License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #endregion #region ================== Namespaces using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Windows.Forms; using CodeImp.DoomBuilder.Config; using CodeImp.DoomBuilder.Data; #endregion namespace CodeImp.DoomBuilder.Controls { public partial class ThingBrowserControl : UserControl { #region ================== Constants private const int WARNING_ICON_INDEX = 20; //mxd private const int FOLDER_ICON_OFFSET = 21; //mxd private const int FOLDER_OPEN_ICON_OFFSET = 41; //mxd #endregion #region ================== Events public delegate void TypeChangedDeletegate(ThingTypeInfo value); public delegate void TypeDoubleClickDeletegate(); public event TypeChangedDeletegate OnTypeChanged; public event TypeDoubleClickDeletegate OnTypeDoubleClicked; #endregion #region ================== Variables private List nodes; private List validnodes; //mxd private ThingTypeInfo thinginfo; private bool doupdatenode; private bool doupdatetextbox; private TreeNode doubleclickednode; //mxd #endregion #region ================== Properties public string TypeStringValue { get { return typeid.Text; } } public bool UseMultiSelection { get { return typelist.SelectionMode == TreeViewSelectionMode.MultiSelectSameLevel; } set { typelist.SelectionMode = (value ? TreeViewSelectionMode.MultiSelectSameLevel : TreeViewSelectionMode.SingleSelect); } } #endregion #region ================== Constructor // Constructor public ThingBrowserControl() { InitializeComponent(); } // This sets up the control public void Setup() { // Go for all predefined categories typelist.Nodes.Clear(); nodes = new List(); validnodes = new List(); //mxd AddThingCategories(General.Map.Data.ThingCategories, typelist.Nodes); //mxd doupdatenode = true; doupdatetextbox = true; } //mxd. This recursively creates thing category tree nodes. Returns true when a thing in this category is obsolete private bool AddThingCategories(ICollection categories, TreeNodeCollection collection) { bool containsobsoletethings = false; foreach(ThingCategory tc in categories) { // Create category TreeNode cn = collection.Add(tc.Name, tc.Title); // Create subcategories bool isobsolete = AddThingCategories(tc.Children, cn.Nodes); // Create things foreach(ThingTypeInfo ti in tc.Things) { // Create thing TreeNode n = cn.Nodes.Add(ti.Title); n.Tag = ti; if(ti.IsObsolete) { n.Text += " - OBSOLETE"; n.BackColor = Color.MistyRose; n.ToolTipText = ti.ObsoleteMessage; // Set warning icon n.ImageIndex = WARNING_ICON_INDEX; n.SelectedImageIndex = WARNING_ICON_INDEX; isobsolete = true; } else { // Set regular icon if((ti.Color > -1) && (ti.Color < WARNING_ICON_INDEX)) n.ImageIndex = ti.Color; n.SelectedImageIndex = n.ImageIndex; } nodes.Add(n); } // Set category icon containsobsoletethings |= isobsolete; if(isobsolete) { cn.BackColor = Color.MistyRose; cn.ImageIndex = WARNING_ICON_INDEX; cn.SelectedImageIndex = WARNING_ICON_INDEX; } else { cn.ImageIndex = FOLDER_ICON_OFFSET; // Offset to folder icons if((tc.Color > -1) && (tc.Color < WARNING_ICON_INDEX)) cn.ImageIndex += tc.Color; cn.SelectedImageIndex = cn.ImageIndex; } } return containsobsoletethings; } #endregion #region ================== Methods // Select a type public void SelectType(int type) { // Set type index typeid.Text = type.ToString(); typeid_TextChanged(this, EventArgs.Empty); } // Return selected type info public ThingTypeInfo GetSelectedInfo() { return thinginfo; } // This clears the type public void ClearSelectedType() { doupdatenode = false; // Clear selection typelist.SelectedNodes.Clear(); //mxd validnodes.Clear(); //mxd typeid.Text = ""; // Collapse nodes foreach(TreeNode n in nodes) if(n.Parent.IsExpanded) n.Parent.Collapse(); doupdatenode = true; } // Result public int GetResult(int original) { //mxd. Get a random ThingTypeInfo from valid nodes? if(typelist.SelectionMode == TreeViewSelectionMode.MultiSelectSameLevel && validnodes.Count > 0) { return (validnodes[General.Random(0, validnodes.Count - 1)].Tag as ThingTypeInfo).Index; } return typeid.GetResult(original); } //mxd public void FocusTextbox() { tbFilter.Focus(); } //mxd private List GetValidNodes() { Dictionary vn = new Dictionary(StringComparer.Ordinal); foreach(TreeNode n in typelist.SelectedNodes) GetValidNodes(n, ref vn); return new List(vn.Values); } private static void GetValidNodes(TreeNode root, ref Dictionary vn) { if(root.Nodes.Count == 0) { if(root.Tag is ThingTypeInfo && !vn.ContainsKey(root.Text)) vn.Add(root.Text, root); } else { foreach(TreeNode n in root.Nodes) GetValidNodes(n, ref vn); } } // Update preview image (mxd) private void UpdateThingSprite() { if(General.Map == null) return; if(thinginfo != null) { if(thinginfo.Sprite.ToLowerInvariant().StartsWith(DataManager.INTERNAL_PREFIX) && (thinginfo.Sprite.Length > DataManager.INTERNAL_PREFIX.Length)) { spritetex.Image = General.Map.Data.GetSpriteImage(thinginfo.Sprite).GetSpritePreview(); return; } if((thinginfo.Sprite.Length < 9) && (thinginfo.Sprite.Length > 0)) { ImageData sprite = General.Map.Data.GetSpriteImage(thinginfo.Sprite); spritetex.Image = sprite.GetPreview(); if(!sprite.IsPreviewLoaded) updatetimer.Start(); return; } } //Show Mixed Things icon? if(validnodes.Count > 1) { spritetex.Image = Properties.Resources.MixedThings; return; } spritetex.Image = null; } #endregion #region ================== Events // List double-clicked. e.Node and typelist.SelectedNodes[0] may contain incorrect node, // so we set the correct one in typelist_AfterSelect handler (mxd) private void typelist_MouseDoubleClick(object sender, MouseEventArgs e) { if(typelist.SelectedNodes.Count == 1 && doubleclickednode != null && doubleclickednode.Nodes.Count == 0 && doubleclickednode.Tag is ThingTypeInfo && OnTypeDoubleClicked != null && typeid.Text.Length > 0) { OnTypeDoubleClicked(); } } // Thing type selection changed private void typelist_SelectionsChanged(object sender, EventArgs e) { doubleclickednode = null; //mxd if(!doupdatetextbox) return; //mxd validnodes = GetValidNodes(); //mxd. Got a valid multiselection? Well, can't show any useful info about that... if(typelist.SelectionMode == TreeViewSelectionMode.MultiSelectSameLevel && validnodes.Count > 1) { doupdatenode = false; if(!string.IsNullOrEmpty(typeid.Text)) { // Event will be raised in typeid_OnTextChanged typeid.Text = ""; } else if(OnTypeChanged != null) { // Or raise event here UpdateThingSprite(); OnTypeChanged(thinginfo); } doupdatenode = true; } else if(validnodes.Count == 1) //Anything selected? { // Show info doupdatenode = false; typeid.Text = (validnodes[0].Tag as ThingTypeInfo).Index.ToString(); doupdatenode = true; // Set as double-clicked only if a single child node is selected if(typelist.SelectedNodes.Count == 1 && typelist.SelectedNodes[0].Nodes.Count == 0) { doubleclickednode = validnodes[0]; //mxd } } else { UpdateThingSprite(); //mxd } } // Thing type index changed private void typeid_TextChanged(object sender, EventArgs e) { bool knownthing = false; // Any text? if(typeid.Text.Length > 0) { // Get the info int typeindex = typeid.GetResult(0); thinginfo = General.Map.Data.GetThingInfoEx(typeindex); if(thinginfo != null) { knownthing = true; // Size sizelabel.Text = (thinginfo.Radius * 2) + " x " + thinginfo.Height; // Hangs from ceiling if(thinginfo.Hangs) positionlabel.Text = "Ceiling"; else positionlabel.Text = "Floor"; // Blocking switch(thinginfo.Blocking) { case ThingTypeInfo.THING_BLOCKING_NONE: blockinglabel.Text = "No"; break; case ThingTypeInfo.THING_BLOCKING_FULL: blockinglabel.Text = "Completely"; break; case ThingTypeInfo.THING_BLOCKING_HEIGHT: blockinglabel.Text = "True-Height"; break; default: blockinglabel.Text = "Unknown"; break; } } if(doupdatenode) { doupdatetextbox = false; typelist.SelectedNodes.Clear(); validnodes.Clear(); //mxd foreach(TreeNode n in nodes) { // Matching node? if((n.Tag as ThingTypeInfo).Index == typeindex) { // Select this if(n.TreeView != null) //mxd. Tree node may've been removed during filtering { if(n.Parent != null) n.Parent.Expand(); // node won't have parent when the list is prefiltered typelist.SelectedNodes.Add(n); n.EnsureVisible(); break; } } } doupdatetextbox = true; } } else { thinginfo = null; if(doupdatenode) { typelist.SelectedNodes.Clear(); validnodes.Clear(); //mxd } } // No known thing? if(!knownthing) { sizelabel.Text = "-"; positionlabel.Text = "-"; blockinglabel.Text = "-"; } //mxd. Update help link bool displayclassname = (thinginfo != null && !string.IsNullOrEmpty(thinginfo.ClassName) && !thinginfo.ClassName.StartsWith("$")); classname.Enabled = (displayclassname && !string.IsNullOrEmpty(General.Map.Config.ThingClassHelp)); classname.Text = (displayclassname ? thinginfo.ClassName : "--"); labelclassname.Enabled = classname.Enabled; // Update icon (mxd) UpdateThingSprite(); // Raise event if(OnTypeChanged != null) OnTypeChanged(thinginfo); } private void updatetimer_Tick(object sender, EventArgs e) { updatetimer.Stop(); UpdateThingSprite(); } //mxd private void typelist_MouseEnter(object sender, EventArgs e) { typelist.Focus(); } //mxd. Transfer focus to Filter textbox private void typelist_KeyPress(object sender, KeyPressEventArgs e) { tbFilter.Focus(); if(e.KeyChar == '\b') // Any better way to check for Backspace?.. { if(!string.IsNullOrEmpty(tbFilter.Text) && tbFilter.SelectionStart > 0 && tbFilter.SelectionLength == 0) { int s = tbFilter.SelectionStart - 1; tbFilter.Text = tbFilter.Text.Remove(s, 1); tbFilter.SelectionStart = s; } } else { tbFilter.AppendText(e.KeyChar.ToString(CultureInfo.InvariantCulture)); } } //mxd private void bClear_Click(object sender, EventArgs e) { tbFilter.Clear(); } //mxd private void tbFilter_TextChanged(object sender, EventArgs e) { typelist.SuspendLayout(); if(string.IsNullOrEmpty(tbFilter.Text.Trim())) { Setup(); typeid_TextChanged(this, EventArgs.Empty); } else { // Go for all predefined categories typelist.SelectedNodes.Clear(); typelist.Nodes.Clear(); validnodes.Clear(); string match = tbFilter.Text.ToUpperInvariant(); HashSet added = new HashSet(StringComparer.OrdinalIgnoreCase); // First add nodes, which titles start with given text foreach(TreeNode node in nodes) { if(node.Text.ToUpperInvariant().StartsWith(match)) { typelist.Nodes.Add(node); added.Add(node.Text); } } // Then add nodes, which titles contain given text foreach(TreeNode node in nodes) { if (!added.Contains(node.Text) && node.Text.ToUpperInvariant().Contains(match)) { typelist.Nodes.Add(node); added.Add(node.Text); } } // Then if the filter is a number, add nodes whose thing number starts with that number if (int.TryParse(tbFilter.Text.Trim(), out int number)) { foreach (TreeNode node in nodes) if (!added.Contains(node.Text) && (node.Tag as ThingTypeInfo).Index.ToString().StartsWith(number.ToString())) { typelist.Nodes.Add(node); added.Add(node.Text); } } doupdatenode = true; doupdatetextbox = true; } typelist.ResumeLayout(); } //mxd. Switch focus to types list? private void tbFilter_KeyUp(object sender, KeyEventArgs e) { if(e.KeyCode == Keys.Down && typelist.Nodes.Count > 0) { typelist.SelectedNodes.Clear(); typelist.SelectedNodes.Add(typelist.Nodes[0]); typelist.Focus(); } } //mxd. Because anchor-based alignment fails when using high-Dpi settings... private void ThingBrowserControl_Resize(object sender, EventArgs e) { infopanel.Top = this.Height - infopanel.Height; infopanel.Width = this.Width; spritepanel.Left = infopanel.Width - spritepanel.Width; typelist.Height = infopanel.Top - typelist.Top; typelist.Width = this.Width; bClear.Left = this.Width - bClear.Width - bClear.Margin.Right; tbFilter.Width = bClear.Left - tbFilter.Left - bClear.Margin.Left; } //mxd. If it's clickable, all data is valid. private void classname_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { General.OpenWebsite(General.Map.Config.ThingClassHelp.Replace("%K", thinginfo.ClassName)); } //mxd. Switch to Open Folder icon private void typelist_BeforeExpand(object sender, TreeViewCancelEventArgs e) { // Category node? if(e.Node.ImageIndex > WARNING_ICON_INDEX) e.Node.ImageIndex = e.Node.ImageIndex - FOLDER_ICON_OFFSET + FOLDER_OPEN_ICON_OFFSET; } //mxd. Switch to Closed Folder icon private void typelist_BeforeCollapse(object sender, TreeViewCancelEventArgs e) { // Category node? if(e.Node.ImageIndex > WARNING_ICON_INDEX) e.Node.ImageIndex = e.Node.ImageIndex - FOLDER_OPEN_ICON_OFFSET + FOLDER_ICON_OFFSET; } #endregion } }