UltimateZoneBuilder/Source/Core/Controls/ThingBrowserControl.cs
spherallic 18b984c455 Allow filtering by action/type number for linedef actions and things
Also remove number prefixes from linedef actions to reduce clutter
2024-04-17 13:03:27 +02:00

546 lines
15 KiB
C#
Executable file

#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<TreeNode> nodes;
private List<TreeNode> 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<TreeNode>();
validnodes = new List<TreeNode>(); //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<ThingCategory> 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<TreeNode> GetValidNodes()
{
Dictionary<string, TreeNode> vn = new Dictionary<string, TreeNode>(StringComparer.Ordinal);
foreach(TreeNode n in typelist.SelectedNodes) GetValidNodes(n, ref vn);
return new List<TreeNode>(vn.Values);
}
private static void GetValidNodes(TreeNode root, ref Dictionary<string, TreeNode> 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<string> added = new HashSet<string>(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
}
}