mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-25 06:16:37 +01:00
77edbd3b1e
* Prevent `long` overflow when VM has a very large amount of memory. Resulted in XenCenter hanging with a significant memory leak. * Remove unused `label` parameter from `LabelShouldBeShown` * Use `double` for increment variable when calculating increments in `ShinyBar` * Use `Debug.Assert` call to avoid issues with `DrawGrid` in `ShinyBar` Signed-off-by: Danilo Del Busso <danilo.delbusso@cloud.com>
362 lines
16 KiB
C#
362 lines
16 KiB
C#
/* 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<Rectangle, string> toolTipRegions = new Dictionary<Rectangle, string>();
|
|
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<Color, Color> _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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="max"></param>
|
|
/// <param name="x"></param>
|
|
/// <returns></returns>
|
|
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<Rectangle, string>();
|
|
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<Rectangle, string> 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;
|
|
}
|
|
}
|
|
}
|