xenadmin/XenAdmin/Controls/Folders/FolderListItem.cs
Konstantina Chremmou 16c88cc7b7 CA-302791: Rewrote the ChangeFolder dialog to fix usability issues.
- Corrected folder selection; reduced clicks required to select folders;
  improved text; added delete button.
- Moved FolderChangeDialogTreeView to the same folder as the other TreeViews;
- Added missing null checks to the MutliSelectTreeView and hid properties
  HScrollPos and VScrollPos from VS's designer (the former was initialised
  to zero every time the designer was updated, causing the treeview images
  to disappear).

Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com>
2018-11-26 10:29:39 +00:00

372 lines
14 KiB
C#

/* 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.Drawing;
using System.Text;
using System.Windows.Forms;
using XenAdmin.Core;
using XenAdmin.Dialogs;
using XenAdmin.Model;
using XenAPI;
namespace XenAdmin.Controls
{
public class FolderListItem
{
public event Action PathChanged;
private const int INNER_PADDING = 9;
private const int RIGHT_PADDING = 20;
private const int IMAGE_OFFSET = 4;
public enum AllowSearch { None, AllButLast, All };
private AllowSearch allowSearch;
private string path;
private List<FLIControl> controls;
public Control Parent;
private int maxWidth = 0;
private bool hasChangeButton;
private LinkLabel changeLabel;
private Padding itemBorder;
public FolderListItem(string path, AllowSearch allowSearch, bool changeButton)
{
this.path = path;
this.controls = new List<FLIControl>();
this.allowSearch = allowSearch;
this.hasChangeButton = changeButton;
this.itemBorder = new Padding(0);
}
public string Path
{
get { return path; }
}
private Color foreColor = SystemColors.ControlText;
public Color ForeColor
{
get { return foreColor; }
set { foreColor = value; }
}
private Font Font
{
get { return Program.DefaultFont; }
}
private Font UnderlineFont
{
get
{
return new Font(Font, FontStyle.Underline);
}
}
public int MaxWidth
{
get { return maxWidth; }
set { maxWidth = value; }
}
public void DrawSelf(Graphics g, Rectangle bounds, bool selected)
{
controls = new List<FLIControl>();
Size bigSize = new Size(int.MaxValue, int.MaxValue);
Point p = new Point(bounds.Left + itemBorder.Left, bounds.Top + itemBorder.Top);
Size sz;
if (path == "")
{
Drawing.DrawText(g, Messages.NONE, Font, p, ForeColor,
g.TextContrast == 5 ? Program.TransparentUsually : Color.Transparent);
sz = Drawing.MeasureText(Messages.NONE, Font);
p.X += sz.Width;
}
else
{
p.X += 3; // this is necessary to compensate for TextFormatFlags.NoPadding below: all the other rows have padding
String[] pathParts = Folders.PointToPath(path);
int trunc1, trunc2;
CalcSizeAndTrunc(g, bounds.Width - itemBorder.Horizontal, out trunc1, out trunc2);
if (trunc1 > 0)
{
Drawing.DrawText(g, Messages.ELLIPSIS, Font, p, ForeColor,
g.TextContrast == 5 ? Program.TransparentUsually : Color.Transparent, TextFormatFlags.NoPadding);
sz = Drawing.MeasureText(g, Messages.ELLIPSIS, Font, bigSize, TextFormatFlags.NoPadding);
p.X += sz.Width + INNER_PADDING;
}
for (int i = trunc1; i < pathParts.Length; ++i)
{
if (i > 0)
{
g.DrawImage(Properties.Resources.folder_separator, p + new Size(0, IMAGE_OFFSET));
p.X += Properties.Resources.folder_separator.Width + INNER_PADDING;
}
string s = pathParts[i];
if (i == pathParts.Length - 1 && trunc2 > 0) // need to truncate final component
s = s.Ellipsise(trunc2);
bool doSearch = (allowSearch == AllowSearch.All ||
(allowSearch == AllowSearch.AllButLast && i != pathParts.Length - 1));
Drawing.DrawText(g, s, Font, p, doSearch ? Color.Blue : ForeColor,
g.TextContrast == 5 ? Program.TransparentUsually : Color.Transparent, TextFormatFlags.NoPadding);
sz = Drawing.MeasureText(g, s, Font, bigSize, TextFormatFlags.NoPadding);
if (doSearch)
controls.Add(new FLIControl(Folders.PathToPoint(pathParts, i + 1),
p.X - bounds.Left, p.Y - bounds.Top, sz.Width, sz.Height));
p.X += sz.Width + INNER_PADDING;
}
}
if (hasChangeButton)
{
p.X += RIGHT_PADDING;
if (changeLabel != null)
{
changeLabel.LinkClicked -= changeLabel_LinkClicked;
changeLabel.Dispose();
}
changeLabel = new LinkLabel();
changeLabel.Text = Messages.CHANGE;
changeLabel.TabStop = true;
changeLabel.LinkClicked += changeLabel_LinkClicked;
changeLabel.Parent = this.Parent;
changeLabel.Left = p.X - bounds.Left;
changeLabel.Top = p.Y - bounds.Top;
}
}
public Size PreferredSize
{
get
{
int t1, t2;
return CalcSizeAndTrunc(int.MaxValue, out t1, out t2);
}
}
// Given the MaxWidth, or the passed-in width if the MaxWidth has not been specified,
// calculate the amount of truncation of the name required,
// and the size of the resultant truncated name.
// trunc1 is the number of components to *remove* from the front of the name
// trunc2 is the number of characters to *retain* in the final component (inc ellipsis if necessary), or zero if not required
private Size CalcSizeAndTrunc(Graphics g, int width, out int trunc1, out int trunc2)
{
Size bigSize = new Size(int.MaxValue, int.MaxValue);
Size theSize = new Size(0, 0);
String[] pathParts = Folders.PointToPath(path);
if (pathParts == null)
{
theSize = Drawing.MeasureText(g, Messages.NONE, Font);
}
else
{
theSize.Width += 3;
for (int i = 0; i < pathParts.Length; ++i)
{
if (i > 0)
theSize.Width += Properties.Resources.folder_separator.Width + INNER_PADDING;
string s = pathParts[i];
Size sz = Drawing.MeasureText(g, s, Font, bigSize, TextFormatFlags.NoPadding);
theSize.Width += sz.Width + INNER_PADDING;
theSize.Height = Math.Max(theSize.Height, sz.Height);
}
}
if (hasChangeButton)
{
theSize.Width += RIGHT_PADDING;
Size size2 = Drawing.MeasureText(g, Messages.CHANGE, UnderlineFont, bigSize, TextFormatFlags.NoPadding);
theSize.Width += size2.Width;
theSize.Height = Math.Max(theSize.Height, size2.Height);
}
else
{
theSize.Width -= INNER_PADDING;
}
theSize.Width += this.itemBorder.Left + this.itemBorder.Right;
theSize.Height += this.itemBorder.Top + this.itemBorder.Bottom;
// theSize is now the width of the item without any truncation.
// Calculate any truncation required.
trunc1 = trunc2 = 0;
if (pathParts != null)
{
int w = MaxWidth;
if (w == 0)
w = width;
for (; theSize.Width > w && trunc1 < pathParts.Length - 1; ++trunc1)
{
// To truncate the first component, we replace the component with an ellipsis.
// Subsequently we delete a separator image and two paddings and the component. (Draw it!).
if (trunc1 == 0)
theSize.Width += Drawing.MeasureText(g, Messages.ELLIPSIS, Font, bigSize, TextFormatFlags.NoPadding).Width;
else
theSize.Width -= Properties.Resources.folder_separator.Width + 2 * INNER_PADDING;
theSize.Width -= Drawing.MeasureText(g, pathParts[trunc1], Font, bigSize, TextFormatFlags.NoPadding).Width;
}
// It may still be that the text is too long, if the last component is very long.
// In that case we need to ellipsise the final component.
// (We just remove one character at a time: binary chop is overkill for normal folder lengths).
if (theSize.Width > w)
{
String s = pathParts[trunc1];
int needToLose = theSize.Width - w;
int widthBefore = Drawing.MeasureText(g, s, Font, bigSize, TextFormatFlags.NoPadding).Width;
for (trunc2 = s.Length + Messages.ELLIPSIS.Length - 1;
trunc2 > Messages.ELLIPSIS.Length &&
widthBefore - Drawing.MeasureText(g, s.Ellipsise(trunc2), Font, bigSize, TextFormatFlags.NoPadding).Width < needToLose;
--trunc2)
;
}
}
return theSize;
}
private Size CalcSizeAndTrunc(int width, out int trunc1, out int trunc2)
{
Graphics g = this.Parent.CreateGraphics();
Size sz = CalcSizeAndTrunc(g, width, out trunc1, out trunc2);
g.Dispose();
return sz;
}
private FLIControl OnControl(Point point)
{
foreach (FLIControl control in controls)
{
if (control.rect.Contains(point))
return control;
}
return null;
}
public void OnMouseMove(Point point)
{
if (OnControl(point) != null)
Cursor = Cursors.Hand;
else
Cursor = Cursors.Default;
}
private Cursor Cursor
{
set
{
if (Parent != null)
Parent.Cursor = value;
}
}
public void OnMouseLeave()
{
Cursor = Cursors.Default;
}
public void OnMouseClick(MouseEventArgs e, Point point)
{
if (e.Button != MouseButtons.Left)
return;
FLIControl control = OnControl(point);
if (control == null)
return;
Program.MainWindow.SearchForFolder(control.tag);
}
private void changeLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
{
if (e.Button != MouseButtons.Left)
return;
LaunchFolderChangeDlg();
}
private void LaunchFolderChangeDlg()
{
using (var dialog = new FolderChangeDialog(path))
if (dialog.ShowDialog(Program.MainWindow) == DialogResult.OK)
{
if (!dialog.FolderChanged)
return;
Folder selectedFolder = dialog.CurrentFolder;
path = selectedFolder == null
? string.Empty
: Folders.AppendPath(selectedFolder.Path, selectedFolder.ToString());
OnPathChanged();
}
}
private void OnPathChanged()
{
if (PathChanged != null)
PathChanged();
}
private class FLIControl
{
public string tag;
public Rectangle rect;
public FLIControl(string tag, Rectangle rect)
{
this.tag = tag;
this.rect = rect;
}
public FLIControl(string tag, int left, int top, int width, int height)
{
this.tag = tag;
this.rect = new Rectangle(left, top, width, height);
}
}
}
}