ZoneBuilder/Source/Core/Controls/ThingBrowserControl.cs
MaxED 022b0474af Fixed inability to disable bilinear filtering in Visual mode some users experienced.
Fixed occasional TreeView flickering in Edit Things window, Browse Action window and Tag Explorer panel.
Updated Thing category icons in the Edit Things window. They now have "opened" and "closed" states.
Internal: added BufferedTreeView to the core controls.
Updated ZDoom game configurations (sector crush mode).
Updated ZDoom ACC.
2023-01-09 12:41:11 +01:00

576 lines
16 KiB
C#

#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;
using CodeImp.DoomBuilder.GZBuilder.Controls;
using static System.Windows.Forms.VisualStyles.VisualStyleElement.Button;
#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;
parametercaption.Visible = General.Map.SRB2;
parameterid.Visible = General.Map.SRB2;
fulltypecaption.Visible = General.Map.SRB2;
fulltypelabel.Visible = General.Map.SRB2;
}
//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);
}
public void SelectType(int type, int parameter)
{
parameterid.Text = parameter.ToString();
SelectType(type);
}
// 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 = "";
parameterid.Text = "";
fulltypelabel.Text = "0";
// 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);
}
public int GetFullType(int original)
{
if (General.Map.SRB2)
return GetResult(original % 4096) + parameterid.GetResult(original / 4096) * 4096;
else
return 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).GetBitmap();
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;
}
// Parameter
parametercaption.Text = thinginfo.ParameterText + ":";
var g = CreateGraphics();
int offset = 2 + (int)g.MeasureString(parametercaption.Text, parametercaption.Font).Width;
parametercaption.Location = new System.Drawing.Point(Math.Max(60 - offset, 0), 27);
parametercaption.Size = new System.Drawing.Size(offset, 13);
parametercaption.BackColor = (parametercaption.Text != "Parameter:") ? Color.LightGray : Color.White;
parametercaption.ForeColor = Color.Black;
}
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 = "-";
parametercaption.Text = "Parameter:";
}
//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();
fulltypelabel.Text = GetFullType(0).ToString();
// Raise event
if(OnTypeChanged != null) OnTypeChanged(thinginfo);
}
//Parameter changed
private void parameterid_WhenTextChanged(object sender, EventArgs e)
{
fulltypelabel.Text = GetFullType(0).ToString();
// 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);
}
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
}
}