/* 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); /// /// 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. /// public readonly List Nodes = new List(); private VisualStyleRenderer plusRenderer; private VisualStyleRenderer minusRenderer; private CustomTreeNode lastSelected; private bool _inUpdate = false; /// /// If you want to make this into a regular listbox, set this to a smaller value, like 5 or something /// 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; } } /// /// The font used in descriptions. /// 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); } } 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 CheckedItems() { List nodes = new List(); foreach (CustomTreeNode node in Nodes) if (node.Level >= 0 && node.State == CheckState.Checked && node.Enabled) nodes.Add(node); return nodes; } public List CheckableItems() { List nodes = new List(); 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; } /// /// Finds next/previous node in Items collection. /// /// Node where the search for next/previous node will start. /// Determines direction of search (search for next or previous node). /// 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]; } /// /// Finds next/previous enabled node in Items collection. /// /// Node where the search for next/previous enabled node will start. /// Determines direction of search (search for next or previous node). /// 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 ItemCheckChanged; protected override void OnMouseDoubleClick(MouseEventArgs 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(); } } 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); } } }