xenadmin/XenAdmin/Controls/SnapshotTreeView.cs

849 lines
29 KiB
C#
Raw Normal View History

/* 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.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Security.Permissions;
using System.Windows.Forms;
using XenCenterLib;
using Message = System.Windows.Forms.Message;
using System.Runtime.InteropServices;
namespace XenAdmin.Controls
{
public partial class SnapshotTreeView : ListView
{
private const int straightLineLength = 8;
private SnapshotIcon root;
//We need this fake thing to make the scrollbars work with the customdrawdate.
private ListViewItem whiteIcon = new ListViewItem();
private Color linkLineColor = SystemColors.ControlDark;
private float linkLineWidth = 2.0f;
private int hGap = 50;
private int vGap = 20;
private readonly CustomLineCap linkLineArrow = new AdjustableArrowCap(4f, 4f, true);
#region Properties
[Browsable(true), Category("Appearance"), Description("Color used to draw connecting lines")]
[DefaultValue(typeof(Color), "ControlDark")]
public Color LinkLineColor
{
get { return linkLineColor; }
set
{
linkLineColor = value;
Invalidate();
}
}
[Browsable(true), Category("Appearance"), Description("Width of connecting lines")]
[DefaultValue(2.0f)]
public float LinkLineWidth
{
get { return linkLineWidth; }
set
{
linkLineWidth = value;
Invalidate();
}
}
[Browsable(true), Category("Appearance"), Description("Horizontal gap between icons")]
[DefaultValue(50)]
public int HGap
{
get { return hGap; }
set
{
if (value < 4 * straightLineLength)
value = 4 * straightLineLength;
hGap = value;
PerformLayout(this, "HGap");
}
}
[Browsable(true), Category("Appearance"), Description("Vertical gap between icons")]
[DefaultValue(20)]
public int VGap
{
get { return vGap; }
set
{
if (value < 0)
value = 0;
vGap = value;
PerformLayout(this, "VGap");
}
}
#endregion
private ImageList imageList = new ImageList();
public SnapshotTreeView(IContainer container)
{
if (container == null)
throw new ArgumentNullException("container");
container.Add(this);
InitializeComponent();
DoubleBuffered = true;
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
base.Items.Add(whiteIcon);
//Init image list
imageList.ColorDepth = ColorDepth.Depth32Bit;
imageList.ImageSize=new Size(32,32);
imageList.Images.Add(Properties.Resources._000_HighLightVM_h32bit_32);
imageList.Images.Add(Properties.Resources.VMTemplate_h32bit_32);
imageList.Images.Add(Properties.Resources.VMTemplate_h32bit_32);
imageList.Images.Add(Properties.Resources._000_VMSnapShotDiskOnly_h32bit_32);
imageList.Images.Add(Properties.Resources._000_VMSnapshotDiskMemory_h32bit_32);
imageList.Images.Add(Properties.Resources._000_ScheduledVMsnapshotDiskOnly_h32bit_32);
imageList.Images.Add(Properties.Resources._000_ScheduledVMSnapshotDiskMemory_h32bit_32);
imageList.Images.Add(Properties.Resources.SpinningFrame0);
imageList.Images.Add(Properties.Resources.SpinningFrame1);
imageList.Images.Add(Properties.Resources.SpinningFrame2);
imageList.Images.Add(Properties.Resources.SpinningFrame3);
imageList.Images.Add(Properties.Resources.SpinningFrame4);
imageList.Images.Add(Properties.Resources.SpinningFrame5);
imageList.Images.Add(Properties.Resources.SpinningFrame6);
imageList.Images.Add(Properties.Resources.SpinningFrame7);
this.LargeImageList = imageList;
}
[SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters",
MessageId = "System.InvalidOperationException.#ctor(System.String)",
Justification = "Indicates a programming error - not user facing")]
internal ListViewItem AddSnapshot(SnapshotIcon snapshot)
{
if (snapshot == null)
throw new ArgumentNullException("snapshot");
if (snapshot.Parent != null)
{
snapshot.Parent.AddChild(snapshot);
snapshot.Parent.Invalidate();
}
else if (root != null)
{
throw new InvalidOperationException("Adding a new root!");
}
else
{
root = snapshot;
}
if (snapshot.ImageIndex == SnapshotIcon.VMImageIndex)
{
//Sort all the parents of the VM to make the path to the VM the first one.
SnapshotIcon current = snapshot;
while (current.Parent != null)
{
if (current.Parent.Children.Count > 1)
{
int indexCurrent = current.Parent.Children.IndexOf(current);
SnapshotIcon temp = current.Parent.Children[0];
current.Parent.Children[0] = current;
current.Parent.Children[indexCurrent] = temp;
}
current.IsInVMBranch = true;
current = current.Parent;
}
}
ListViewItem item = Items.Add(snapshot);
if (Items.Count == 1)
Items.Add(whiteIcon);
return item;
}
public new void Clear()
{
base.Clear();
RemoveSnapshot(root);
}
internal void RemoveSnapshot(SnapshotIcon snapshot)
{
if (snapshot != null && snapshot.Parent != null)
{
IList<SnapshotIcon> siblings = snapshot.Parent.Children;
int pos = siblings.IndexOf(snapshot);
siblings.Remove(snapshot);
// add our children in our place
foreach (SnapshotIcon child in snapshot.Children)
{
siblings.Insert(pos++, child);
child.Parent = snapshot.Parent;
}
snapshot.Parent.Invalidate();
snapshot.Parent = null;
}
else
{
root = null;
}
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
if (SelectedItems.Count == 1)
{
SnapshotIcon item = SelectedItems[0] as SnapshotIcon;
if (item != null && !item.Selectable)
{
item.Selected = false;
item.Focused = false;
}
}
else if (this.SelectedItems.Count > 1)
{
foreach (ListViewItem item in SelectedItems)
{
SnapshotIcon snapItem = item as SnapshotIcon;
if (snapItem != null && !snapItem.Selectable)
{
item.Selected = false;
item.Focused = false;
}
}
}
base.OnSelectedIndexChanged(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
ListViewItem item = this.HitTest(e.X, e.Y).Item;
if (item == null)
{
ListViewItem item2 = this.HitTest(e.X, e.Y - 23).Item;
if (item2 != null)
{
item2.Selected = true;
item2.Focused = true;
}
else
{
base.OnMouseUp(new MouseEventArgs(e.Button, e.Clicks, e.X, e.Y - 23, e.Delta));
return;
}
}
base.OnMouseUp(e);
}
#region Layout
protected override void OnLayout(LayoutEventArgs levent)
{
if (root != null && this.Parent != null)
{
//This is needed to maximize and minimize properly, there is some issue in the ListView Control
Win32.POINT pt = new Win32.POINT();
IntPtr hResult = SendMessage(Handle, LVM_GETORIGIN, IntPtr.Zero, ref pt);
origin = pt;
root.InvalidateAll();
int x = Math.Max(this.HGap, this.Size.Width / 2 - root.SubtreeWidth / 2);
int y = Math.Max(this.VGap, this.Size.Height / 2 - root.SubtreeHeight / 2);
PositionSnapshots(root, x, y);
Invalidate();
whiteIcon.Position = new Point(x + origin.X, y + root.SubtreeHeight - 20 + origin.Y);
}
}
protected override void OnParentChanged(EventArgs e)
{
//We need to cancel the parent changes when we change to the other view
}
private void PositionSnapshots(SnapshotIcon icon, int x, int y)
{
try
{
Size iconSize = icon.DefaultSize;
Point newPoint = new Point(x, y + icon.CentreHeight - iconSize.Height / 2);
icon.Position = new Point(newPoint.X + origin.X, newPoint.Y + origin.Y);
x += iconSize.Width + HGap;
for (int i = 0; i < icon.Children.Count; i++)
{
SnapshotIcon child = icon.Children[i];
PositionSnapshots(child, x, y);
y += child.SubtreeHeight;
}
}
catch (Exception)
{
// Debugger.Break();
}
}
public const int LVM_GETORIGIN = 0x1000 + 41;
private Win32.POINT origin = new Win32.POINT();
[DllImport("user32.dll")]
internal static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, ref Win32.POINT pt);
public override Size GetPreferredSize(Size proposedSize)
{
if (root == null && Parent != null)
{
return DefaultSize;
}
return new Size(root.SubtreeWidth, root.SubtreeHeight);
}
#endregion
#region Drawing
private bool m_empty = false;
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
protected override void WndProc(ref Message m)
{
const int WM_HSCROLL = 0x0114;
const int WM_VSCROLL = 0x0115;
const int WM_MOUSEWHEEL = 0x020A;
const int WM_PAINT = 0x000F;
base.WndProc(ref m);
if (m.Msg == WM_HSCROLL || m.Msg == WM_VSCROLL || (m.Msg == WM_MOUSEWHEEL && (IsVerticalScrollBarVisible(this) || IsHorizontalScrollBarVisible(this))))
{
Invalidate();
}
if (m.Msg == WM_PAINT)
{
Graphics gg = CreateGraphics();
if (this.Items.Count == 0)
{
m_empty = true;
Graphics g = CreateGraphics();
string text = Messages.SNAPSHOTS_EMPTY;
SizeF proposedSize = g.MeasureString(text, Font, 275);
float x = this.Width / 2 - proposedSize.Width / 2;
float y = this.Height / 2 - proposedSize.Height / 2;
RectangleF rect = new RectangleF(x, y, proposedSize.Width, proposedSize.Height);
using (var brush = new SolidBrush(BackColor))
g.FillRectangle(brush, rect);
g.DrawString(text, Font, Brushes.Black, rect);
}
}
else if (m_empty && this.Items.Count > 0)
{
m_empty = false;
this.Invalidate();
}
}
private const int WS_HSCROLL = 0x100000;
private const int WS_VSCROLL = 0x200000;
private const int GWL_STYLE = (-16);
[System.Runtime.InteropServices.DllImport("user32",
CharSet = System.Runtime.InteropServices.CharSet.Auto)]
private static extern int GetWindowLong(IntPtr hwnd, int nIndex);
internal static bool IsVerticalScrollBarVisible(Control ctrl)
{
if (!ctrl.IsHandleCreated)
return false;
return (GetWindowLong(ctrl.Handle, GWL_STYLE) & WS_VSCROLL) != 0;
}
internal static bool IsHorizontalScrollBarVisible(Control ctrl)
{
if (!ctrl.IsHandleCreated)
return false;
return (GetWindowLong(ctrl.Handle, GWL_STYLE) & WS_HSCROLL) != 0;
}
private void SnapshotTreeView_DrawItem(object sender, DrawListViewItemEventArgs e)
{
if (this.Parent != null)
{
e.DrawDefault = true;
SnapshotIcon icon = e.Item as SnapshotIcon;
if (icon == null)
return;
DrawDate(e, icon, false);
if (icon.Parent != null)
PaintLine(e.Graphics, icon.Parent, icon, icon.IsInVMBranch);
for (int i = 0; i < icon.Children.Count; i++)
{
SnapshotIcon child = icon.Children[i];
PaintLine(e.Graphics, icon, child, child.IsInVMBranch);
}
}
}
public void DrawRoundRect(Graphics g, Brush b, float x, float y, float width, float height, float radius)
{
GraphicsPath gp = new GraphicsPath();
gp.AddLine(x + radius, y, x + width - (radius * 2), y); // Line
gp.AddArc(x + width - (radius * 2), y, radius * 2, radius * 2, 270, 90); // Corner
gp.AddLine(x + width, y + radius, x + width, y + height - (radius * 2)); // Line
gp.AddArc(x + width - (radius * 2), y + height - (radius * 2), radius * 2, radius * 2, 0, 90); // Corner
gp.AddLine(x + width - (radius * 2), y + height, x + radius, y + height); // Line
gp.AddArc(x, y + height - (radius * 2), radius * 2, radius * 2, 90, 90); // Corner
gp.AddLine(x, y + height - (radius * 2), x, y + radius); // Line
gp.AddArc(x, y, radius * 2, radius * 2, 180, 90); // Corner
gp.CloseFigure();
g.FillPath(b, gp);
}
private void DrawDate(DrawListViewItemEventArgs e, SnapshotIcon icon, bool background)
{
StringFormat stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
//Time
int timeX = e.Bounds.X;
int timeY = e.Bounds.Y + e.Bounds.Height;
Size proposedSizeName = new Size(e.Bounds.Width,
Int32.MaxValue);
Size timeSize = TextRenderer.MeasureText(e.Graphics, icon.LabelCreationTime,
new Font(this.Font.FontFamily, this.Font.Size - 1), proposedSizeName, TextFormatFlags.WordBreak);
timeSize = new Size(e.Bounds.Width, timeSize.Height);
Rectangle timeRect = new Rectangle(new Point(timeX, timeY), timeSize);
if (background)
{
e.Graphics.FillRectangle(Brushes.GreenYellow, timeRect);
}
e.Graphics.DrawString(icon.LabelCreationTime, new Font(this.Font.FontFamily, this.Font.Size - 1), Brushes.Black, timeRect, stringFormat);
}
private void PaintLine(Graphics g, SnapshotIcon icon, SnapshotIcon child, bool highlight)
{
if (child.Index == -1)
return;
try
{
Rectangle leftItemBounds = icon.GetBounds(ItemBoundsPortion.Entire);
Rectangle rightItemBounds = child.GetBounds(ItemBoundsPortion.Entire);
leftItemBounds.Size = icon.DefaultSize;
rightItemBounds.Size = child.DefaultSize;
int left = leftItemBounds.Right + 6;
int right = rightItemBounds.Left;
int mid = (left + right) / 2;
Point start = new Point(left, (leftItemBounds.Bottom + leftItemBounds.Top) / 2);
Point end = new Point(right, (rightItemBounds.Top + rightItemBounds.Bottom) / 2);
Point curveStart = start;
curveStart.Offset(straightLineLength, 0);
Point curveEnd = end;
curveEnd.Offset(-straightLineLength, 0);
Point control1 = new Point(mid + straightLineLength, start.Y);
Point control2 = new Point(mid - straightLineLength, end.Y);
Color lineColor = LinkLineColor;
float lineWidth = LinkLineWidth;
if (highlight)
{
lineColor = Color.ForestGreen;
lineWidth = 2.5f;
}
using (Pen p = new Pen(lineColor, lineWidth))
{
p.SetLineCap(LineCap.Round, LineCap.Custom, DashCap.Flat);
p.CustomEndCap = linkLineArrow;
g.SmoothingMode = SmoothingMode.AntiAlias;
GraphicsPath path = new GraphicsPath();
path.AddLine(start, curveStart);
path.AddBezier(curveStart, control1, control2, curveEnd);
path.AddLine(curveEnd, end);
g.DrawPath(p, path);
}
}
catch (Exception)
{
//Debugger.Break();
}
}
#endregion
private string _spinningMessage = "";
public string SpinningMessage { get { return _spinningMessage; } }
internal void ChangeVMToSpinning(bool p, string message)
{
_spinningMessage = message;
foreach (var item in Items)
{
SnapshotIcon snapshotIcon = item as SnapshotIcon;
if (snapshotIcon != null && (snapshotIcon.ImageIndex == SnapshotIcon.VMImageIndex || snapshotIcon.ImageIndex > SnapshotIcon.UnknownImage))
{
if (string.IsNullOrEmpty(message))
{
snapshotIcon.ChangeSpinningIcon(p, _spinningMessage);
}
else
{
snapshotIcon.ChangeSpinningIcon(p, _spinningMessage);
}
return;
}
}
}
}
internal class SnapshotIcon : ListViewItem
{
public const int VMImageIndex = 0;
public const int Template = 1;
public const int CustomTemplate = 2;
public const int DiskSnapshot = 3;
public const int DiskAndMemorySnapshot = 4;
public const int ScheduledDiskSnapshot = 5;
public const int ScheduledDiskMemorySnapshot = 6;
public const int UnknownImage = 6;
private SnapshotIcon parent;
private readonly SnapshotTreeView treeView;
private readonly List<SnapshotIcon> children = new List<SnapshotIcon>();
private readonly string _name;
private readonly string _creationTime;
public Size DefaultSize = new Size(70, 64);
private Timer spinningTimer = new Timer();
#region Cached dimensions
private int subtreeWidth;
private int subtreeHeight;
private int subtreeWeight;
private int centreHeight;
#endregion
#region Properties
public string LabelCreationTime
{
get { return _creationTime; }
}
public string LabelName
{
get { return _name; }
}
internal SnapshotIcon Parent
{
get { return parent; }
set { parent = value; }
}
internal IList<SnapshotIcon> Children
{
get { return children; }
}
internal bool Selectable
{
get
{
// It's selectable if it's a snapshot; otherwise not
return ImageIndex == DiskSnapshot || ImageIndex == DiskAndMemorySnapshot || ImageIndex == ScheduledDiskSnapshot || ImageIndex == ScheduledDiskMemorySnapshot;
}
}
#endregion
public SnapshotIcon(string name, string createTime, SnapshotIcon parent, SnapshotTreeView treeView, int imageIndex)
: base(name.Ellipsise(35))
{
this._name = name.Ellipsise(35);
this._creationTime = createTime;
this.parent = parent;
this.treeView = treeView;
this.UseItemStyleForSubItems = false;
this.ToolTipText = String.Format("{0} {1}", name, createTime);
this.ImageIndex = imageIndex;
if (imageIndex == SnapshotIcon.VMImageIndex)
{
spinningTimer.Tick += new EventHandler(timer_Tick);
spinningTimer.Interval = 150;
}
}
private int currentSpinningFrame = 7;
private void timer_Tick(object sender, EventArgs e)
{
this.ImageIndex = currentSpinningFrame <= 14 ? currentSpinningFrame++ : currentSpinningFrame = 7;
}
internal void ChangeSpinningIcon(bool enabled, string message)
{
if (this.ImageIndex > UnknownImage || this.ImageIndex == VMImageIndex)
{
this.ImageIndex = enabled ? 7 : VMImageIndex;
this.Text = enabled ? message : Messages.NOW;
if (enabled)
spinningTimer.Start();
else
spinningTimer.Stop();
}
}
private bool _isInVMBranch = false;
public bool IsInVMBranch
{
get { return _isInVMBranch; }
set { _isInVMBranch = value; }
}
internal void AddChild(SnapshotIcon icon)
{
children.Add(icon);
Invalidate();
}
public override void Remove()
{
SnapshotTreeView view = (SnapshotTreeView)ListView;
view.RemoveSnapshot(this);
base.Remove();
view.PerformLayout();
}
/// <summary>
/// Causes the item and its ancestors to forget their cached dimensions.
/// </summary>
public void Invalidate()
{
subtreeWeight = 0;
subtreeHeight = 0;
centreHeight = 0;
subtreeWidth = 0;
if (parent != null)
parent.Invalidate();
}
/// <summary>
/// Causes the item and all its descendents to forget their cached dimensions.
/// </summary>
public void InvalidateAll()
{
subtreeWeight = 0;
subtreeHeight = 0;
centreHeight = 0;
subtreeWidth = 0;
foreach (SnapshotIcon icon in children)
{
icon.InvalidateAll();
}
}
#region Layout/Dimensions
public int SubtreeWidth
{
get
{
if (subtreeWidth == 0)
{
int currentWidth = this.DefaultSize.Width + treeView.HGap;
if (children.Count > 0)
{
int maxWidth = 0;
foreach (SnapshotIcon icon in children)
{
int childSubtree = icon.SubtreeWidth;
if (currentWidth + childSubtree > maxWidth)
maxWidth = currentWidth + childSubtree;
}
if (maxWidth > currentWidth)
subtreeWidth = maxWidth;
else
subtreeWidth = currentWidth;
}
}
return subtreeWidth;
}
}
/// <summary>
/// Height of the subtree rooted at this node, including the margin above and below.
/// </summary>
public int SubtreeHeight
{
get
{
if (subtreeHeight == 0)
{
subtreeHeight = this.DefaultSize.Height + treeView.VGap;
if (children.Count > 0)
{
int height = 0;
foreach (SnapshotIcon icon in children)
{
height += icon.SubtreeHeight; // recurse
}
if (height > subtreeHeight)
subtreeHeight = height;
}
}
return subtreeHeight;
}
}
/// <summary>
/// The number of items rooted at this node (including the node itself)
/// </summary>
public int SubtreeWeight
{
get
{
if (subtreeWeight == 0)
{
int weight = 1; // this
foreach (SnapshotIcon icon in children)
{
weight += icon.SubtreeWeight;
}
subtreeWeight = weight;
}
return subtreeWeight;
}
}
/// <summary>
/// The weighted mean centre height for this node, within the range 0 - SubtreeHeight
/// </summary>
public int CentreHeight
{
get
{
if (centreHeight == 0)
{
int top = 0;
int totalWeight = 0;
int weightedCentre = 0;
foreach (SnapshotIcon icon in children)
{
int iconWeight = icon.SubtreeWeight;
totalWeight += iconWeight;
weightedCentre += iconWeight * (top + icon.CentreHeight); // recurse
top += icon.SubtreeHeight;
}
if (totalWeight > 0)
centreHeight = weightedCentre / totalWeight;
else
centreHeight = (top + SubtreeHeight) / 2;
}
return centreHeight;
}
}
#endregion
}
}