mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-12-22 16:36:03 +01:00
599 lines
22 KiB
C#
599 lines
22 KiB
C#
/* Copyright (c) Citrix Systems Inc.
|
|
* All rights reserved.
|
|
*
|
|
* 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.Data;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
using System.Windows.Forms.VisualStyles;
|
|
using XenAdmin.Core;
|
|
using XenAPI;
|
|
|
|
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 int NodeIndent
|
|
{
|
|
get { return _nodeIndent; }
|
|
set { _nodeIndent = value; }
|
|
}
|
|
|
|
public CustomTreeNode SecretNode = new CustomTreeNode();
|
|
|
|
private bool _showCheckboxes = true;
|
|
[Browsable(true)]
|
|
public bool ShowCheckboxes
|
|
{
|
|
get { return _showCheckboxes; }
|
|
set { _showCheckboxes = value; }
|
|
}
|
|
|
|
private bool _showDescription = true;
|
|
[Browsable(true)]
|
|
public bool ShowDescription
|
|
{
|
|
get { return _showDescription; }
|
|
set { _showDescription = value; }
|
|
}
|
|
|
|
private bool _showImages = false;
|
|
[Browsable(true)]
|
|
public 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(Properties.Resources.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(Properties.Resources.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);
|
|
}
|
|
}
|
|
}
|