xenadmin/XenAdmin/Controls/CustomTreeView.cs
Konstantina Chremmou d7b519a53c Updated copyright notice on files.
Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>
2023-01-30 16:24:16 +00:00

593 lines
22 KiB
C#

/* Copyright (c) Cloud Software Group, Inc.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.VisualStyles;
using XenAdmin.Core;
namespace XenAdmin.Controls
{
public partial class CustomTreeView : FlickerFreeListBox
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// SURGEON GENERAL'S WARNING: This collection contains the infamous 'secret node'.
/// To iterate only through items that you have explicity added to the treeview, use
/// the Items collection instead.
/// </summary>
public readonly List<CustomTreeNode> Nodes = new List<CustomTreeNode>();
private VisualStyleRenderer plusRenderer;
private VisualStyleRenderer minusRenderer;
private CustomTreeNode lastSelected;
private bool _inUpdate = false;
/// <summary>
/// If you want to make this into a regular listbox, set this to a smaller value, like 5 or something
/// </summary>
private int _nodeIndent = 19;
[Browsable(true)]
public virtual int NodeIndent
{
get { return _nodeIndent; }
set { _nodeIndent = value; }
}
public CustomTreeNode SecretNode = new CustomTreeNode();
private bool _showCheckboxes = true;
[Browsable(true)]
public virtual bool ShowCheckboxes
{
get { return _showCheckboxes; }
set { _showCheckboxes = value; }
}
private bool _showDescription = true;
[Browsable(true)]
public virtual bool ShowDescription
{
get { return _showDescription; }
set { _showDescription = value; }
}
private bool _showImages = false;
[Browsable(true)]
public virtual bool ShowImages
{
get { return _showImages; }
set { _showImages = value; }
}
/// <summary>
/// The font used in descriptions.
/// </summary>
private Font _descriptionFont = null;
public override Font Font
{
get
{
return base.Font;
}
set
{
base.Font = value;
if (_descriptionFont != null)
_descriptionFont.Dispose();
_descriptionFont = new Font(value.FontFamily, value.Size - 1);
RecalculateWidth();
}
}
private bool _showRootLines = true;
[Browsable(true)]
public bool ShowRootLines
{
get { return _showRootLines; }
set { _showRootLines = value; }
}
private bool _rootAlwaysExpanded = false;
[Browsable(true)]
public bool RootAlwaysExpanded
{
get { return _rootAlwaysExpanded; }
set { _rootAlwaysExpanded = value; }
}
public override int ItemHeight { get { return 17; } }
public CustomTreeView()
{
InitializeComponent();
_descriptionFont = new Font(base.Font.FontFamily, base.Font.Size - 2);
if (Application.RenderWithVisualStyles)
{
plusRenderer = new VisualStyleRenderer(VisualStyleElement.TreeView.Glyph.Closed);
minusRenderer = new VisualStyleRenderer(VisualStyleElement.TreeView.Glyph.Opened);
}
}
public new void BeginUpdate()
{
_inUpdate = true;
base.BeginUpdate();
}
public new void EndUpdate()
{
_inUpdate = false;
base.EndUpdate();
RecalculateWidth();
Resort();
Refresh();
}
public new void Invalidate()
{
RecalculateWidth();
base.Invalidate();
}
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
if(Enabled)
{
using (SolidBrush backBrush = new SolidBrush(BackColor))
{
e.Graphics.FillRectangle(backBrush, e.Bounds);
}
}
else
{
e.Graphics.FillRectangle(SystemBrushes.Control, e.Bounds);
}
if (e.Index == -1 || Items.Count <= e.Index)
return;
CustomTreeNode node = this.Items[e.Index] as CustomTreeNode;
if (node == null)
return;
//int indent = (node.Level + 1) * NodeIndent;
int indent = node.Level * NodeIndent + (ShowRootLines ? NodeIndent : 2);
int TextLength = Drawing.MeasureText(node.ToString(), e.Font).Width + 2;
int TextLeft = indent + (ShowCheckboxes && !node.HideCheckbox ? ItemHeight : 0) + (ShowImages ? ItemHeight : 0);
//CA-59618: add top margin to the items except the first one when rendering with
//visual styles because in this case there is already one pixel of margin.
int topMargin = Application.RenderWithVisualStyles && e.Index == 0 ? 0 : 1;
if (Enabled && node.Selectable)
{
Color nodeBackColor = node.Enabled
? e.BackColor
: (e.BackColor == BackColor ? BackColor : SystemColors.ControlLight);
using (SolidBrush backBrush = new SolidBrush(nodeBackColor))
{
e.Graphics.FillRectangle(backBrush, new Rectangle(e.Bounds.Left + TextLeft + 1, e.Bounds.Top + topMargin, TextLength - 4, e.Bounds.Height));
}
}
//draw expander
if (node.ChildNodes.Count > 0 && (ShowRootLines || node.Level > 0))
{
if (!node.Expanded)
{
if(Application.RenderWithVisualStyles)
plusRenderer.DrawBackground(e.Graphics, new Rectangle(e.Bounds.Left + indent - ItemHeight, e.Bounds.Top + 3 + topMargin, 9, 9));
else
e.Graphics.DrawImage(Images.StaticImages.tree_plus, new Rectangle(e.Bounds.Left + indent - ItemHeight, e.Bounds.Top + 3 + topMargin, 9, 9));
}
else
{
if (Application.RenderWithVisualStyles)
minusRenderer.DrawBackground(e.Graphics, new Rectangle(e.Bounds.Left + indent - ItemHeight, e.Bounds.Top + 3 + topMargin, 9, 9));
else
e.Graphics.DrawImage(Images.StaticImages.tree_minus, new Rectangle(e.Bounds.Left + indent - ItemHeight, e.Bounds.Top + 3 + topMargin, 9, 9));
}
}
//draw checkboxes
if (ShowCheckboxes && !node.HideCheckbox)
{
var checkedState = CheckBoxState.UncheckedDisabled;
if (node.State == CheckState.Checked)
{
if (node.Enabled && Enabled)
checkedState = CheckBoxState.CheckedNormal;
else if (node.CheckedIfdisabled)
checkedState = CheckBoxState.CheckedDisabled;
}
else if (node.State == CheckState.Indeterminate)
{
checkedState = node.Enabled && Enabled
? CheckBoxState.MixedNormal
: CheckBoxState.MixedDisabled;
}
else if (node.State == CheckState.Unchecked)
{
checkedState = node.Enabled && Enabled
? CheckBoxState.UncheckedNormal
: CheckBoxState.UncheckedDisabled;
}
CheckBoxRenderer.DrawCheckBox(e.Graphics, new Point(e.Bounds.Left + indent, e.Bounds.Top + 1 + topMargin), checkedState);
indent += ItemHeight;
}
//draw images
if (ShowImages && node.Image != null)
{
var rectangle = new Rectangle(e.Bounds.Left + indent, e.Bounds.Top + topMargin, node.Image.Width, node.Image.Height);
if (node.Enabled && Enabled)
e.Graphics.DrawImage(node.Image, rectangle);
else
e.Graphics.DrawImage(node.Image, rectangle, 0, 0, node.Image.Width, node.Image.Height, GraphicsUnit.Pixel, Drawing.GreyScaleAttributes);
indent += ItemHeight;
}
//draw item's main text
Color textColor = node.Enabled && Enabled
? (node.Selectable ? e.ForeColor : ForeColor)
: SystemColors.GrayText;
Drawing.DrawText(e.Graphics, node.ToString(), e.Font, new Point(e.Bounds.Left + indent, e.Bounds.Top + topMargin), textColor);
indent += TextLength;
//draw item's description
if (ShowDescription)
{
Drawing.DrawText(e.Graphics, node.Description, _descriptionFont, new Point(e.Bounds.Left + indent, e.Bounds.Top + 1 + topMargin), SystemColors.GrayText);
}
}
public List<CustomTreeNode> CheckedItems()
{
List<CustomTreeNode> nodes = new List<CustomTreeNode>();
foreach (CustomTreeNode node in Nodes)
if (node.Level >= 0 && node.State == CheckState.Checked && node.Enabled)
nodes.Add(node);
return nodes;
}
public List<CustomTreeNode> CheckableItems()
{
List<CustomTreeNode> nodes = new List<CustomTreeNode>();
foreach (CustomTreeNode node in Nodes)
if (node.Level >= 0 && node.State != CheckState.Checked && node.Enabled)
nodes.Add(node);
return nodes;
}
public void AddNode(CustomTreeNode node)
{
if (Nodes.Count == 0)
Nodes.Add(SecretNode);
SecretNode.AddChild(node);
Nodes.Add(node);
if (!_inUpdate)
{
RecalculateWidth();
Resort();
Refresh();
}
}
public void RemoveNode(CustomTreeNode node)
{
Nodes.Remove(node);
if (!_inUpdate)
{
RecalculateWidth();
Resort();
Refresh();
}
}
public void AddChildNode(CustomTreeNode parent, CustomTreeNode child)
{
parent.AddChild(child);
Nodes.Add(child);
if (!_inUpdate)
{
RecalculateWidth();
Resort();
Refresh();
}
}
public void ClearAllNodes()
{
Nodes.Clear();
if (!_inUpdate)
{
RecalculateWidth();
Resort();
Refresh();
}
}
public void Resort()
{
try
{
lastSelected = SelectedItem as CustomTreeNode;
}
catch (IndexOutOfRangeException)
{
// Accessing ListBox.SelectedItem sometimes throws an IndexOutOfRangeException (See CA-24396)
log.Warn("IndexOutOfRangeException in ListBox.SelectedItem");
lastSelected = null;
}
Nodes.Sort();
Items.Clear();
foreach (CustomTreeNode node in Nodes)
{
if (node.Level != -1 && node.ParentNode.Expanded)
Items.Add(node);
}
SelectedItem = lastSelected;
// I've yet to come across the above assignement working. If we fail to restore the selection, select something so the user can see focus feedback
// (the color of the selected item is the only indication as to whether it is focused or not)
// Iterating through and using CustomTreeNode.equals is useless here as it compares based on index, which I think is why the above call almost never works
if (SelectedItem == null && lastSelected != null && Items.Count > 0)
{
SelectedItem = Items[0];
}
}
// Adjusts the width of the control to that of the widest row
private void RecalculateWidth()
{
int maxWidth = 0;
foreach (CustomTreeNode node in this.Nodes)
{
int indent = (node.Level + 1) * NodeIndent;
int checkbox = ShowCheckboxes && !node.HideCheckbox ? ItemHeight : 0;
int image = ShowImages ? ItemHeight : 0;
int text = Drawing.MeasureText(node.ToString(), this.Font).Width + 2;
int desc = ShowDescription ? Drawing.MeasureText(node.Description, _descriptionFont).Width : 0;
int itemWidth = indent + checkbox + image + text + desc + 10;
maxWidth = Math.Max(itemWidth, maxWidth);
}
// Set horizontal extent and enable scrollbar if necessary
this.HorizontalExtent = maxWidth;
this.HorizontalScrollbar = this.HorizontalExtent > this.Width && Enabled;
}
/// <summary>
/// Finds next/previous node in Items collection.
/// </summary>
/// <param name="currentNode">Node where the search for next/previous node will start.</param>
/// <param name="searchForward">Determines direction of search (search for next or previous node).</param>
/// <returns></returns>
protected CustomTreeNode GetNextNode(CustomTreeNode currentNode, bool searchForward)
{
if (currentNode == null)
return null;
int index = Items.IndexOf(currentNode);
if (searchForward)
{
index++;
if (index >= Items.Count)
index = -1;
}
else
index--;
if (index < 0)
return null;
return (CustomTreeNode)Items[index];
}
/// <summary>
/// Finds next/previous enabled node in Items collection.
/// </summary>
/// <param name="currentNode">Node where the search for next/previous enabled node will start.</param>
/// <param name="searchForward">Determines direction of search (search for next or previous node).</param>
/// <returns></returns>
protected CustomTreeNode GetNextEnabledNode(CustomTreeNode currentNode, bool searchForward)
{
if (currentNode == null)
return null;
CustomTreeNode nextNode = GetNextNode(currentNode, searchForward);
if (nextNode == null)
return null;
if (nextNode.Enabled)
return nextNode;
return GetNextEnabledNode(nextNode, searchForward);
}
protected override void OnMouseUp(MouseEventArgs e)
{
bool anythingChanged = false;
bool orderChanged = false;
Point loc = this.PointToClient(MousePosition);
int index = this.IndexFromPoint(loc);
if (index < 0 || index > Items.Count)
return;
CustomTreeNode node = this.Items[index] as CustomTreeNode;
if (node == null)
return;
int indent = node.Level * NodeIndent + (ShowRootLines ? NodeIndent : 2);
if (node.ChildNodes.Count > 0 && loc.X < indent - (ItemHeight - 9) && loc.X > indent - ItemHeight &&
(ShowRootLines || node.Level > 0))
{
node.Expanded = !node.Expanded;
node.PreferredExpanded = node.Expanded;
anythingChanged = true;
orderChanged = true;
}
else if (ShowCheckboxes && !node.HideCheckbox && node.Enabled && loc.X > indent && loc.X < indent + ItemHeight)
{
if (node.State == CheckState.Unchecked || node.State == CheckState.Indeterminate)
node.State = CheckState.Checked;
else
node.State = CheckState.Unchecked;
anythingChanged = true;
}
if (orderChanged)
Resort();
if (anythingChanged)
{
if(ItemCheckChanged != null)
ItemCheckChanged(node, new EventArgs());
Refresh();
}
base.OnMouseUp(e);
}
public event EventHandler<EventArgs> ItemCheckChanged;
public event EventHandler DoubleClickOnRow;
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
bool anythingChanged = false;
Point loc = this.PointToClient(MousePosition);
int index = this.IndexFromPoint(loc);
if (index < 0 || index > Items.Count)
return;
CustomTreeNode node = this.Items[index] as CustomTreeNode;
if (node == null)
return;
int indent = node.Level * NodeIndent + (ShowRootLines ? NodeIndent : 2);
if (node.ChildNodes.Count > 0 && loc.X < indent - (ItemHeight - 9) && loc.X > indent - ItemHeight &&
(ShowRootLines || node.Level > 0))
{
return;
}
else if (ShowCheckboxes && !node.HideCheckbox && loc.X > indent && loc.X < indent + ItemHeight)
{
return;
}
else if (node.ChildNodes.Count > 0 && (node.Level > 0 || !_rootAlwaysExpanded))
{
node.Expanded = !node.Expanded;
node.PreferredExpanded = node.Expanded;
anythingChanged = true;
}
if (anythingChanged)
{
Resort();
Refresh();
}
if (DoubleClickOnRow != null)
DoubleClickOnRow(this, e);
}
protected override void OnKeyUp(KeyEventArgs e)
{
var node = SelectedItem as CustomTreeNode;
switch (e.KeyCode)
{
case Keys.Space:
{
if (!ShowCheckboxes)
break;
if (node == null || node.HideCheckbox || !node.Enabled)
break;
//checked => uncheck it; unchecked or indeterminate => check it
node.State = node.State == CheckState.Checked ? CheckState.Unchecked : CheckState.Checked;
Refresh();
if (ItemCheckChanged != null)
ItemCheckChanged(node, new EventArgs());
break;
}
}
base.OnKeyUp(e);
}
protected override void OnKeyDown(KeyEventArgs e)
{
var node = SelectedItem as CustomTreeNode;
switch (e.KeyCode)
{
case Keys.Right:
{
if (node != null && node.ChildNodes.Count > 0 && (node.Level > 0 || !_rootAlwaysExpanded) && !node.Expanded)
{
node.Expanded = true;
Resort();
Refresh();
e.Handled = true;
}
break;
}
case Keys.Left:
{
if (node != null && node.ChildNodes.Count > 0 && (node.Level > 0 || !_rootAlwaysExpanded) && node.Expanded)
{
node.Expanded = false;
Resort();
Refresh();
e.Handled = true;
}
break;
}
}
base.OnKeyDown(e);
}
}
}