/* 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.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Windows.Forms; using XenAdmin.Core; namespace XenAdmin.Controls.Ballooning { public partial class ShinyBar : UserControl { private static readonly Font font = new Font("Segoe UI", 9); private const int radius = 5; private const int pad = 2; private const int TEXT_PAD = 3; private const int TEXT_FADE = 8; // Because one bar has several regions with different tooltips, we handle the tooltip timing etc. ourselves. private ToolTip toolTip = new ToolTip(); private Dictionary toolTipRegions = new Dictionary(); private Timer toolTipTimer = new Timer(); private const int TOOLTIP_DELAY = 500; private Point lastMouseLocation; private Point toolTipLocation; private string toolTipText; private bool ignoreNextOnMouseMove; //palette protected Color VMShinyBar_Used = Color.ForestGreen; protected Color HostShinyBar_Xen = Color.DarkGray; protected Color HostShinyBar_ControlDomain = Color.DimGray; protected Color[] HostShinyBar_VMs = { Color.MidnightBlue, Color.SteelBlue }; protected Color[] GpuShinyBar_VMs = { Color.FromArgb(83, 39, 139), Color.FromArgb(157, 115, 215) }; protected Color ShinyBar_Unused = Color.Black; protected Color ShinyBar_Text = Color.White; protected Color Grid = Color.DarkGray; protected Color SliderLimits = Color.LightGray; private readonly Func _colorTransformer = ControlPaint.LightLight; public ShinyBar() { InitializeComponent(); toolTip.Popup += toolTip_Popup; toolTipTimer.Interval = TOOLTIP_DELAY; toolTipTimer.Tick += toolTipTimer_Tick; } protected void DrawToTarget(Graphics g, Rectangle barBounds, Rectangle segmentBounds, Color color) { DrawToTarget(g, barBounds, segmentBounds, color, "", Color.Empty, HorizontalAlignment.Left, ""); } protected void DrawToTarget(Graphics g, Rectangle barBounds, Rectangle segmentBounds, Color color, string text, Color textColor, HorizontalAlignment alignment, string toolTipText) { // The width shouldn't normally be < 0, but free space can be transiently < 0 while the metrics catch up, // especially across a reboot of a VM. if (segmentBounds.Width <= 0) return; // We draw the whole bar for each segment, but clip to the region corresponding to that segment (as if we // were viewing it through a succession of windows). This is the easiest way to make it the right shape. g.Clip = new Region(segmentBounds); g.SmoothingMode = SmoothingMode.AntiAlias; using (GraphicsPath outerPath = GetRoundedPath(barBounds, radius)) { // Outer rounded rectangle { Color topColor = color; Color bottomColor = _colorTransformer(color); using (LinearGradientBrush outerBrush = new LinearGradientBrush(barBounds, topColor, bottomColor, LinearGradientMode.Vertical)) { g.FillPath(outerBrush, outerPath); } } } // Render text // Text is rendered behind the 'reflection'. It is centered within the segment, unless the segment is too narrow to fit all the text, // in which case the text is rendered TEXT_PAD pixels from the left edge. If the text would extend into the TEXT_PAD pixels from // the right edge of the segment, then the right TEXT_FADE pixels of the text are faded out. if (!String.IsNullOrEmpty(text)) { g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; StringFormat sf = new StringFormat(); SizeF textSize = g.MeasureString(text, font); float horizPos; switch (alignment) { case HorizontalAlignment.Left: horizPos = segmentBounds.Left + TEXT_PAD; break; case HorizontalAlignment.Right: // NB If the caption is multi-line, it's still left-aligned within its box: // just the box is right-aligned within the segment. horizPos = segmentBounds.Right - textSize.Width - TEXT_PAD; break; default: // HorizontalAlignment.Center: horizPos = segmentBounds.Left + (segmentBounds.Width - textSize.Width) / 2; break; } RectangleF textRect = new RectangleF(horizPos, segmentBounds.Top + (segmentBounds.Height - textSize.Height * 0.9f) / 2, textSize.Width, textSize.Height); if (textRect.X < segmentBounds.X + TEXT_PAD) { textRect.Offset((segmentBounds.X + TEXT_PAD) - textRect.X, 0); } // Render text to a bitmap... using (Bitmap bitmap = new Bitmap((int)(segmentBounds.Width + 1), (int)(segmentBounds.Height + 1), PixelFormat.Format32bppArgb)) { using (Graphics gBitmap = Graphics.FromImage(bitmap)) { gBitmap.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; gBitmap.Clip = new Region(new Rectangle(TEXT_PAD, 0, bitmap.Width - 2 * TEXT_PAD, bitmap.Height)); textRect.X -= segmentBounds.Left; textRect.Y -= segmentBounds.Top; using (SolidBrush textBrush = new SolidBrush(textColor)) gBitmap.DrawString(text, font, textBrush, textRect, sf); // ...if we can't fit all the text in, overlay fade... if (textSize.Width + (2 * TEXT_PAD) > bitmap.Width) { for (int x = bitmap.Width - TEXT_FADE - TEXT_PAD; x < bitmap.Width - TEXT_PAD; x++) { if (x < 0) { // For very narrow segments, don't try to paint outside segment continue; } double i = (x - bitmap.Width + TEXT_FADE + TEXT_PAD) / (double)TEXT_FADE; for (int y = 0; y < bitmap.Height; y++) { Color c = bitmap.GetPixel(x, y); int alpha = (int)(c.A - (i * c.A)); c = Color.FromArgb(alpha, c.R, c.G, c.B); bitmap.SetPixel(x, y, c); } } } } // ...and render bitmap to final image Point location = new Point((int)segmentBounds.X, (int)segmentBounds.Y); g.DrawImage(bitmap, location); } } // Inner rounded rectangle RectangleF rect = new RectangleF(barBounds.X + pad, barBounds.Y + pad, barBounds.Width - (2f * pad), barBounds.Height * 0.49f); using (GraphicsPath innerPath = GetRoundedPath(rect, radius - pad)) { int alphaTop = 120, alphaBottom = 30; Color topColor = Color.FromArgb(alphaTop, Color.White); Color bottomColor = Color.FromArgb(alphaBottom, Color.White); rect = new RectangleF(rect.X, rect.Y, rect.Width, rect.Height + pad + 1); using (LinearGradientBrush lighterBrush = new LinearGradientBrush(rect, topColor, bottomColor, LinearGradientMode.Vertical)) { g.FillPath(lighterBrush, innerPath); } } // Remember the tooltip text if (!string.IsNullOrEmpty(toolTipText)) toolTipRegions[segmentBounds] = toolTipText; // Reset the clip region g.Clip = new Region(); } private static GraphicsPath GetRoundedPath(RectangleF rect, float radius) { float diameter = 2 * radius; GraphicsPath path = new GraphicsPath(); path.StartFigure(); path.AddArc(rect.X, rect.Y, diameter, diameter, 180, 90); path.AddArc(rect.Right - diameter, rect.Y, diameter, diameter, 270, 90); path.AddArc(rect.Right - diameter, rect.Bottom - diameter - 1, diameter, diameter, 0, 90); path.AddArc(rect.X, rect.Bottom - diameter - 1, diameter, diameter, 90, 90); path.CloseFigure(); return path; } protected void DrawGrid(Graphics g, Rectangle barArea, double bytesPerPixel, double max) { Debug.Assert(max > 0, "Memory value should be larger than zero"); const int min_gap = 40; // min gap between consecutive labels (which are on alternate ticks) const int line_height = 12; int line_bottom = barArea.Top + barArea.Height / 2; int line_top = barArea.Top - line_height; int text_bottom = line_top - 2; // Find the size of the longest label string label = string.Format(Messages.VAL_MB, Util.ToMB(max, RoundingBehaviour.Nearest)); Size labelSize = Drawing.MeasureText(g, label, Program.DefaultFont, TextFormatFlags.NoPadding); int longest = labelSize.Width; int text_top = text_bottom - labelSize.Height; // Calculate a suitable increment var incr = Util.BINARY_MEGA / 2.0; while(incr / bytesPerPixel * 2 < min_gap + longest) incr *= 2; // Draw the grid using (Pen pen = new Pen(Grid)) { bool withLabel = true; for (var x = 0.0; x <= max; x += incr) { // Tick int pos = barArea.Left + (int)((double)x / bytesPerPixel); g.DrawLine(pen, pos, line_top, pos, line_bottom); // Label if (withLabel) { label = Util.MemorySizeStringSuitableUnits(x, false); Size size = Drawing.MeasureText(g, label, Program.DefaultFont); Rectangle rect = new Rectangle(new Point(pos - size.Width/2, text_top), size); if (LabelShouldBeShown(max, x)) { Drawing.DrawText(g, label, Program.DefaultFont, rect, Grid, Color.Transparent); } } withLabel = !withLabel; } } } /// /// There are 2 cases: /// 1. If the maximum is smaller or equal to 1 GB, then show all the labels. /// 2. If the maximum is greater than 1 GB, then show only the labels that are a multiple of half a GB. /// /// /// /// private static bool LabelShouldBeShown(double max, double x) { return max <= Util.BINARY_GIGA || (x % (0.5 * Util.BINARY_GIGA)) == 0; } protected virtual Rectangle barRect { get { return new Rectangle(20, 30, this.Width - 45, barHeight); } } protected virtual int barHeight { get { return 20; } } public override void Refresh() { toolTipRegions = new Dictionary(); base.Refresh(); } protected override void OnMouseMove(MouseEventArgs e) { if (lastMouseLocation != e.Location) { // CA-38305: on some PCs OnMouseMove gets repeatedly called while the mouse cursor is still. // this if statement ensures that the code below is only called while the mouse is actually moving. if (ignoreNextOnMouseMove) { // The reason for this is that when the tooltip pops up, we get an // additional onMouseMove which would otherwise hide the tooltip again. ignoreNextOnMouseMove = false; return; } toolTipTimer.Stop(); toolTip.Hide(this); lastMouseLocation = e.Location; toolTipTimer.Start(); } } protected override void OnMouseLeave(EventArgs e) { toolTipTimer.Stop(); toolTip.Hide(this); } private void toolTipTimer_Tick(object sender, EventArgs e) { foreach (KeyValuePair pair in toolTipRegions) { if (pair.Key.Contains(lastMouseLocation)) { ShowTooltip(pair.Value, lastMouseLocation); toolTipTimer.Stop(); } } } private void ShowTooltip(string text, Point location) { toolTipText = text; toolTipLocation = location; toolTip.Show(text, this, location); } private void toolTip_Popup(object sender, PopupEventArgs e) { Point correctLocation = lastMouseLocation + new Size(1, -e.ToolTipSize.Height); // put it above-right of the mouse if (toolTipLocation != correctLocation) { ShowTooltip(toolTipText, correctLocation); return; } ignoreNextOnMouseMove = true; } } }