/* 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 XenAdmin.Controls; using XenAdmin.Core; 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 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 children = new List(); 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 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(); } /// /// Causes the item and its ancestors to forget their cached dimensions. /// public void Invalidate() { subtreeWeight = 0; subtreeHeight = 0; centreHeight = 0; subtreeWidth = 0; if (parent != null) parent.Invalidate(); } /// /// Causes the item and all its descendents to forget their cached dimensions. /// 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; } } /// /// Height of the subtree rooted at this node, including the margin above and below. /// 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; } } /// /// The number of items rooted at this node (including the node itself) /// public int SubtreeWeight { get { if (subtreeWeight == 0) { int weight = 1; // this foreach (SnapshotIcon icon in children) { weight += icon.SubtreeWeight; } subtreeWeight = weight; } return subtreeWeight; } } /// /// The weighted mean centre height for this node, within the range 0 - SubtreeHeight /// 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 } }