710 lines
21 KiB
C#
710 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Linq;
|
|
using System.Windows.Forms;
|
|
using JD.Kernel.StaticHelpers;
|
|
|
|
namespace JD.UserControls.QuickOpenDialogs
|
|
{
|
|
/// <summary>
|
|
/// Prompts the user with a list of possible items to select and allows to filter with text.
|
|
/// </summary>
|
|
public class QuickOpenForm<T>
|
|
{
|
|
#region Constants
|
|
|
|
private const int MainBoxHeight = 43;
|
|
private const int TextBoxHeight = 18;
|
|
private const int MainBoxWidth = 440;
|
|
private static readonly Size DefaultImageSize = new Size(16, 16);
|
|
private static readonly Pen ms_borderPen = new Pen(System.Drawing.Color.FromArgb(0x33, 0x99, 0xFF));
|
|
private static readonly StringFormat ms_hintFormat = new StringFormat { Alignment = StringAlignment.Far, Trimming = StringTrimming.EllipsisPath };
|
|
private static readonly Size TextPadding = new Size(2, 1);
|
|
private static readonly Size FormPadding = new Size(5, 3);
|
|
|
|
#endregion
|
|
|
|
#region Variables
|
|
|
|
private readonly int m_maxListHeight;
|
|
private Size m_imageSize;
|
|
private T m_selectedItem;
|
|
private Icon m_image;
|
|
private string m_hintText;
|
|
|
|
private IEnumerable<T> m_originalItems;
|
|
private List<ListBoxItem> m_items;
|
|
private readonly QuickOpenControl m_control;
|
|
private readonly Popup m_popup;
|
|
private bool m_showFrame;
|
|
|
|
#endregion
|
|
|
|
#region Constructor
|
|
|
|
public QuickOpenForm(IEnumerable<T> items)
|
|
{
|
|
Throw.IfNull(items, "items");
|
|
|
|
m_originalItems = items;
|
|
m_control = new QuickOpenControl();
|
|
m_control.Activated += m_control_Activated;
|
|
m_popup = new Popup(m_control)
|
|
{
|
|
UseFadeEffect = false,
|
|
DisposeOnClose = true,
|
|
Width = MainBoxWidth
|
|
};
|
|
|
|
m_maxListHeight = ListBox.Height;
|
|
ListBox.DrawItem += listBox_DrawItem;
|
|
ListBox.MouseDown += ListBox_MouseDown;
|
|
ListBox.SelectedIndexChanged += ListBox_SelectedIndexChanged;
|
|
|
|
TextBox.TextChanged += textBox_TextChanged;
|
|
m_control.Paint += m_control_Paint;
|
|
|
|
SetListVisibility(false);
|
|
ImageSize = DefaultImageSize;
|
|
ResizeTextBox();
|
|
ListBox.Top = MainBoxHeight;
|
|
ListBox.Left = 0;
|
|
|
|
// Redirect the mouse wheel to the list box so the user doesnt have to click in the list.
|
|
TextBox.MouseWheel += (s, e) => ListBox.Focus();
|
|
|
|
NameProvider = item => item != null ? item.ToString() : "";
|
|
SortItems = true;
|
|
SortOrderProvider = input => 0;
|
|
ShowOnEmptySearchProvider = input => false;
|
|
ItemMatcher = DefaultItemMatcher;
|
|
|
|
// Force the list to be initialized when it pops (useful when we specified that we want the list to be filled when the search text is empty).
|
|
m_control.Load += (s, e) => textBox_TextChanged(null, EventArgs.Empty);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Properties
|
|
|
|
public T SelectedItem
|
|
{
|
|
get
|
|
{
|
|
var item = ListBox.SelectedItem as ListBoxItem;
|
|
return item != null ? item.Item : default(T);
|
|
}
|
|
set
|
|
{
|
|
if (!Equals(value, null))
|
|
{
|
|
var itemName = NameProvider(value);
|
|
|
|
var item = ListBox.Items.OfType<ListBoxItem>().FirstOrDefault(search => Equals(search.Item, value)) ??
|
|
ListBox.Items.OfType<ListBoxItem>().FirstOrDefault(search => search.Text == itemName);
|
|
|
|
if (item != null)
|
|
{
|
|
ListBox.SelectedItem = item;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public IEnumerable<T> Items
|
|
{
|
|
get { return m_originalItems; }
|
|
set
|
|
{
|
|
m_originalItems = value;
|
|
m_items = null;
|
|
textBox_TextChanged(null, null);
|
|
}
|
|
}
|
|
|
|
public Size ImageSize
|
|
{
|
|
get { return m_imageSize; }
|
|
set
|
|
{
|
|
m_imageSize = value;
|
|
ListBox.ItemHeight = System.Math.Max(ListBox.ItemHeight, value.Height);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Image paint on lower left corner.
|
|
/// </summary>
|
|
public Icon Image
|
|
{
|
|
get { return m_image; }
|
|
set
|
|
{
|
|
m_image = value;
|
|
ResizeTextBox();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Text written on title of form.
|
|
/// </summary>
|
|
public string Caption
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public string HintText
|
|
{
|
|
get { return m_hintText; }
|
|
set
|
|
{
|
|
if (m_hintText != value)
|
|
{
|
|
m_hintText = value;
|
|
m_control.Invalidate(new Rectangle(0, 0, m_control.Width, TextBox.Location.Y));
|
|
}
|
|
}
|
|
}
|
|
|
|
public string SearchText
|
|
{
|
|
get { return TextBox.Text; }
|
|
set { TextBox.Text = value; }
|
|
}
|
|
|
|
public bool ShowAllOnEmptyText { get; set; }
|
|
|
|
/// <summary>
|
|
/// True if the item should be visible even when the user did not enter any search.
|
|
/// </summary>
|
|
/// <remarks>You can also show all on an empty search with the flag <see cref="ShowAllOnEmptyText"/></remarks>
|
|
public Converter<T, bool> ShowOnEmptySearchProvider
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Converter<Keys, bool> ProcessDialogKey
|
|
{
|
|
get { return m_control.ProcessDialogKeyEvent; }
|
|
set { m_control.ProcessDialogKeyEvent = value; }
|
|
}
|
|
|
|
private Converter<T, string> _nameProvider;
|
|
|
|
public Converter<T, string> NameProvider
|
|
{
|
|
get { return _nameProvider; }
|
|
set
|
|
{
|
|
if (value != null)
|
|
_nameProvider = value;
|
|
}
|
|
}
|
|
|
|
public Converter<T, string> HintProvider
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Converter<T, int> SortOrderProvider
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public delegate TextQuery.MatchResult ItemMatcherDelegate(T item, TextQuery query);
|
|
|
|
public ItemMatcherDelegate ItemMatcher
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Converter<T, Image> ImageProvider
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public event Action<T> ItemClick;
|
|
public event Action<T> ItemAccepted;
|
|
|
|
public event Action<Graphics, Rectangle, T> ItemBackgroundPaint;
|
|
|
|
public event EventHandler Closed
|
|
{
|
|
add { m_popup.Disposed += value; }
|
|
remove { m_popup.Disposed -= value; }
|
|
}
|
|
|
|
public event EventHandler SearchTextChanged
|
|
{
|
|
add { TextBox.TextChanged += value; }
|
|
remove { TextBox.TextChanged -= value; }
|
|
}
|
|
|
|
private TextQuery.MatchResult DefaultItemMatcher(T item, TextQuery query)
|
|
{
|
|
return query.Matches(NameProvider(item));
|
|
}
|
|
|
|
public System.Drawing.Color BackgroundColor
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Multi Selection
|
|
|
|
public IEnumerable<T> SelectedItems
|
|
{
|
|
get { return ListBox.SelectedItems.OfType<ListBoxItem>().Select(result => result.Item); }
|
|
}
|
|
|
|
public void ClearSelectedItems()
|
|
{
|
|
ListBox.SelectedItems.Clear();
|
|
}
|
|
|
|
void ListBox_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
SelectionChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Triggered when items are selected in the list.
|
|
/// </summary>
|
|
public event Action SelectionChanged;
|
|
|
|
public SelectionMode SelectionMode
|
|
{
|
|
get { return ListBox.SelectionMode; }
|
|
set { ListBox.SelectionMode = value; }
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private Properties
|
|
|
|
private ListBox ListBox
|
|
{
|
|
get { return m_control.ListBox; }
|
|
}
|
|
|
|
private TextBox TextBox
|
|
{
|
|
get { return m_control.textBox; }
|
|
}
|
|
|
|
public bool ShowFrame
|
|
{
|
|
get { return m_showFrame; }
|
|
set
|
|
{
|
|
m_showFrame = value;
|
|
textBox_TextChanged(null, null);
|
|
}
|
|
}
|
|
|
|
public bool SortItems { get; set; }
|
|
|
|
public void Show()
|
|
{
|
|
Show(false);
|
|
}
|
|
|
|
public void Show(bool activate)
|
|
{
|
|
var screen = Form.ActiveForm != null ? Screen.FromControl(Form.ActiveForm) : Screen.FromPoint(Control.MousePosition);
|
|
|
|
int x = screen.Bounds.X + (screen.Bounds.Width - m_control.Width) / 2;
|
|
int y = screen.Bounds.Y + (screen.Bounds.Height - MainBoxHeight) / 3;
|
|
|
|
if (activate)
|
|
{
|
|
NativeMethods.SetForegroundWindowEx(m_popup.Handle);
|
|
}
|
|
|
|
m_popup.Show(x, y);
|
|
}
|
|
|
|
private void ResizeTextBox()
|
|
{
|
|
TextBox.AutoSize = false;
|
|
TextBox.Left = m_image != null ? FormPadding.Width + m_imageSize.Width + FormPadding.Width : FormPadding.Width;
|
|
TextBox.Width = MainBoxWidth - TextBox.Left - FormPadding.Width;
|
|
TextBox.Height = TextBoxHeight;
|
|
}
|
|
|
|
private static void DrawClearRectangle(Graphics graphics, Rectangle bounds)
|
|
{
|
|
graphics.FillRectangle(new SolidBrush(System.Drawing.Color.FromArgb(0xFC, 0xFC, 0xFC)), bounds);
|
|
}
|
|
|
|
private static void DrawSelectionRectangle(Graphics graphics, Rectangle bounds)
|
|
{
|
|
bounds = new Rectangle(bounds.X, bounds.Y, bounds.Width - 1, bounds.Height - 1);
|
|
graphics.FillRectangle(new SolidBrush(System.Drawing.Color.FromArgb(75, 0x33, 0x99, 0xFF)), bounds);
|
|
graphics.DrawRectangle(new Pen(System.Drawing.Color.FromArgb(0x33, 0x99, 0xFF)), bounds);
|
|
}
|
|
|
|
private void SetListVisibility(bool visible)
|
|
{
|
|
ListBox.Visible = visible;
|
|
|
|
Region region;
|
|
|
|
if (m_showFrame)
|
|
{
|
|
region = new Region(m_control.Bounds);
|
|
}
|
|
else
|
|
{
|
|
region = new Region(new Rectangle(0, 0, MainBoxWidth, MainBoxHeight));
|
|
if (visible)
|
|
{
|
|
region.Union(ListBox.Bounds);
|
|
}
|
|
}
|
|
|
|
m_control.Region = region;
|
|
}
|
|
|
|
private bool AcceptSelection()
|
|
{
|
|
int current = ListBox.SelectedIndex;
|
|
|
|
if (!ListBox.Visible || current < 0)
|
|
{
|
|
return AcceptEmptySelection(TextBox.Text);
|
|
}
|
|
|
|
m_selectedItem = ((ListBoxItem)ListBox.Items[current]).Item;
|
|
m_popup.Close();
|
|
|
|
ItemAccepted?.Invoke(m_selectedItem);
|
|
return true;
|
|
}
|
|
|
|
protected virtual bool AcceptEmptySelection(string searchQuery)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public void InvalidateListBox()
|
|
{
|
|
ListBox.Invalidate();
|
|
}
|
|
|
|
public void Invalidate(T item)
|
|
{
|
|
for (int i = 0; i < ListBox.Items.Count; i++)
|
|
{
|
|
if (Equals(((ListBoxItem)ListBox.Items[i]).Item, item))
|
|
{
|
|
ListBox.Invalidate(ListBox.GetItemRectangle(i));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Event Handlers
|
|
|
|
void ListBox_MouseDown(object sender, MouseEventArgs e)
|
|
{
|
|
var index = ListBox.IndexFromPoint(e.Location);
|
|
if (index >= 0)
|
|
{
|
|
var clickedItem = (ListBoxItem)ListBox.Items[index];
|
|
ItemClick?.Invoke(clickedItem.Item);
|
|
}
|
|
}
|
|
|
|
private Brush CreateBackgroundBrush(Rectangle rectangle)
|
|
{
|
|
if (!BackgroundColor.IsEmpty)
|
|
return new SolidBrush(BackgroundColor);
|
|
|
|
return new LinearGradientBrush(rectangle, System.Drawing.Color.FromArgb(251, 251, 251), System.Drawing.Color.FromArgb(240, 240, 240), LinearGradientMode.Vertical);
|
|
}
|
|
|
|
private void m_control_Paint(object sender, PaintEventArgs e)
|
|
{
|
|
var graphics = e.Graphics;
|
|
|
|
var rectangle = new Rectangle(0, 0, MainBoxWidth - 1, MainBoxHeight - 1);
|
|
if (ShowFrame)
|
|
{
|
|
rectangle = m_control.Bounds;
|
|
}
|
|
|
|
using (var backBrush = CreateBackgroundBrush(rectangle))
|
|
{
|
|
graphics.FillRectangle(backBrush, rectangle);
|
|
}
|
|
|
|
graphics.DrawRectangle(ms_borderPen, rectangle);
|
|
|
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
|
|
|
string caption = Caption.NonEmpty("Enter filter:");
|
|
graphics.DrawString(caption, m_control.Font, GetBestBrushForBackground(System.Drawing.Color.Black, System.Drawing.Color.White), FormPadding.Width, FormPadding.Height);
|
|
|
|
if (!string.IsNullOrEmpty(HintText))
|
|
{
|
|
var captionWidth = graphics.MeasureString(caption, m_control.Font).Width;
|
|
|
|
// Some spacing
|
|
captionWidth += 20;
|
|
|
|
var hintTextWidth = graphics.MeasureString(HintText, m_control.Font).Width;
|
|
|
|
if (captionWidth + hintTextWidth < m_control.Width)
|
|
graphics.DrawString(HintText, m_control.Font, GetBestBrushForBackground(System.Drawing.Color.LightGray, System.Drawing.Color.DarkGray), m_control.Width - FormPadding.Width - hintTextWidth, FormPadding.Height);
|
|
}
|
|
|
|
if (m_image != null)
|
|
{
|
|
graphics.DrawIcon(m_image, new Rectangle(FormPadding.Width, TextBox.Top, 16, 16));
|
|
}
|
|
}
|
|
|
|
private void m_control_Activated(object sender, CancelEventArgs e)
|
|
{
|
|
e.Cancel = !AcceptSelection();
|
|
}
|
|
|
|
private IEnumerable<ListBoxItem> GetItemsToSearchIn()
|
|
{
|
|
if (m_items == null)
|
|
{
|
|
m_items = new List<ListBoxItem>();
|
|
foreach (var item in m_originalItems)
|
|
{
|
|
m_items.Add(new ListBoxItem(item)
|
|
{
|
|
Text = NameProvider(item),
|
|
SortOrder = SortOrderProvider(item),
|
|
});
|
|
}
|
|
}
|
|
|
|
return m_items;
|
|
}
|
|
|
|
private void textBox_TextChanged(object sender, EventArgs e)
|
|
{
|
|
ListBox.BeginUpdate();
|
|
try
|
|
{
|
|
ListBox.Items.Clear();
|
|
|
|
var query = new TextQuery(TextBox.Text);
|
|
|
|
List<ListBoxItem> selection = new List<ListBoxItem>();
|
|
|
|
bool onlyItemsOfEmptySearch = string.IsNullOrEmpty(TextBox.Text) && !ShowAllOnEmptyText;
|
|
|
|
foreach (var item in GetItemsToSearchIn())
|
|
{
|
|
if (onlyItemsOfEmptySearch)
|
|
{
|
|
if (!ShowOnEmptySearchProvider(item.Item))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
item.MatchResult = ItemMatcher(item.Item, query);
|
|
|
|
if (item.IsMatch)
|
|
selection.Add(item);
|
|
|
|
if (selection.Count >= 1000)
|
|
break;
|
|
}
|
|
|
|
if (SortItems)
|
|
{
|
|
selection.Sort();
|
|
}
|
|
|
|
ListBox.Items.AddRange(selection.Cast<object>().ToArray());
|
|
|
|
if (ListBox.Items.Count >= 1)
|
|
{
|
|
ListBox.SelectedIndex = 0;
|
|
if (m_showFrame)
|
|
{
|
|
ListBox.Height = m_maxListHeight;
|
|
}
|
|
else
|
|
{
|
|
ListBox.Height = System.Math.Min((ListBox.Items.Count + 1) * ListBox.ItemHeight, m_maxListHeight);
|
|
}
|
|
SetListVisibility(true);
|
|
}
|
|
else
|
|
{
|
|
ListBox.SelectedIndex = -1;
|
|
SetListVisibility(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ListBox.EndUpdate();
|
|
}
|
|
|
|
OnSearchTextChanged();
|
|
}
|
|
|
|
protected virtual void OnSearchTextChanged()
|
|
{
|
|
}
|
|
|
|
private void listBox_DrawItem(object sender, DrawItemEventArgs e)
|
|
{
|
|
Size ImagePadding = new Size(1, 0);
|
|
|
|
var item = (ListBoxItem)ListBox.Items[e.Index];
|
|
|
|
var graphics = e.Graphics;
|
|
var bounds = e.Bounds;
|
|
|
|
DrawClearRectangle(graphics, bounds);
|
|
|
|
if (((e.State & DrawItemState.Selected) == DrawItemState.Selected))
|
|
{
|
|
DrawSelectionRectangle(graphics, bounds);
|
|
}
|
|
|
|
if (ItemBackgroundPaint != null)
|
|
{
|
|
ItemBackgroundPaint(graphics, bounds, item.Item);
|
|
}
|
|
|
|
var imageProvider = ImageProvider;
|
|
if (imageProvider != null)
|
|
{
|
|
var image = imageProvider(item.Item);
|
|
|
|
if (image != null)
|
|
{
|
|
e.Graphics.DrawImage(image, new Rectangle(e.Bounds.X + ImagePadding.Width, e.Bounds.Y, m_imageSize.Width, m_imageSize.Height));
|
|
}
|
|
}
|
|
|
|
string name = item.Text;
|
|
PointF textPosition = new PointF(m_imageSize.Width + ImagePadding.Width + TextPadding.Width, e.Bounds.Y + TextPadding.Height);
|
|
e.Graphics.DrawString(name, m_control.Font, Brushes.Black, textPosition);
|
|
|
|
var hintProvider = HintProvider;
|
|
if (hintProvider != null)
|
|
{
|
|
var hint = hintProvider(item.Item);
|
|
if (!string.IsNullOrEmpty(hint))
|
|
{
|
|
SizeF textSize = e.Graphics.MeasureString(name, m_control.Font);
|
|
textSize.Width += m_imageSize.Width + ImagePadding.Width + TextPadding.Width * 2;
|
|
|
|
RectangleF hintBounds = new RectangleF(e.Bounds.X + textSize.Width, e.Bounds.Y + TextPadding.Height, e.Bounds.Width - TextPadding.Width - textSize.Width, e.Bounds.Height - TextPadding.Height);
|
|
e.Graphics.DrawString(hint, m_control.Font, Brushes.DimGray, hintBounds, ms_hintFormat);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Nested
|
|
|
|
private class ListBoxItem : IComparable<ListBoxItem>
|
|
{
|
|
public ListBoxItem(T item)
|
|
{
|
|
Item = item;
|
|
}
|
|
|
|
public T Item
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public string Text
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public int SortOrder
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
internal TextQuery.MatchResult MatchResult
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public bool IsMatch
|
|
{
|
|
get { return MatchResult.IsMatch; }
|
|
}
|
|
|
|
public int CompareTo(ListBoxItem other)
|
|
{
|
|
int sortOrder = SortOrder.CompareTo(other.SortOrder);
|
|
if (sortOrder != 0)
|
|
{
|
|
return sortOrder;
|
|
}
|
|
|
|
int result = MatchResult.CompareTo(other.MatchResult);
|
|
if (result != 0)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
return string.Compare(Text, other.Text, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return Item != null ? Item.ToString() : "";
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private double GetLight(System.Drawing.Color color)
|
|
{
|
|
return (0.2126 * color.R + 0.7152 * color.G + 0.0722 * color.B) / 255;
|
|
}
|
|
|
|
private Brush GetBestBrushForBackground(System.Drawing.Color color1, System.Drawing.Color color2)
|
|
{
|
|
if (BackgroundColor.IsEmpty)
|
|
return new SolidBrush(color1);
|
|
|
|
var bg = GetLight(BackgroundColor);
|
|
var c1 = GetLight(color1);
|
|
var c2 = GetLight(color2);
|
|
|
|
var c1Diff = System.Math.Abs(bg - c1);
|
|
var c2Diff = System.Math.Abs(bg - c2);
|
|
|
|
return new SolidBrush(c1Diff > c2Diff ? color1 : color2);
|
|
}
|
|
}
|
|
}
|