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 { /// /// Prompts the user with a list of possible items to select and allows to filter with text. /// public class QuickOpenForm { #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 m_originalItems; private List m_items; private readonly QuickOpenControl m_control; private readonly Popup m_popup; private bool m_showFrame; #endregion #region Constructor public QuickOpenForm(IEnumerable 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().FirstOrDefault(search => Equals(search.Item, value)) ?? ListBox.Items.OfType().FirstOrDefault(search => search.Text == itemName); if (item != null) { ListBox.SelectedItem = item; } } } } public IEnumerable 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); } } /// /// Image paint on lower left corner. /// public Icon Image { get { return m_image; } set { m_image = value; ResizeTextBox(); } } /// /// Text written on title of form. /// 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; } /// /// True if the item should be visible even when the user did not enter any search. /// /// You can also show all on an empty search with the flag public Converter ShowOnEmptySearchProvider { get; set; } public Converter ProcessDialogKey { get { return m_control.ProcessDialogKeyEvent; } set { m_control.ProcessDialogKeyEvent = value; } } private Converter _nameProvider; public Converter NameProvider { get { return _nameProvider; } set { if (value != null) _nameProvider = value; } } public Converter HintProvider { get; set; } public Converter SortOrderProvider { get; set; } public delegate TextQuery.MatchResult ItemMatcherDelegate(T item, TextQuery query); public ItemMatcherDelegate ItemMatcher { get; set; } public Converter ImageProvider { get; set; } public event Action ItemClick; public event Action ItemAccepted; public event Action 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 SelectedItems { get { return ListBox.SelectedItems.OfType().Select(result => result.Item); } } public void ClearSelectedItems() { ListBox.SelectedItems.Clear(); } void ListBox_SelectedIndexChanged(object sender, EventArgs e) { SelectionChanged?.Invoke(); } /// /// Triggered when items are selected in the list. /// 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 GetItemsToSearchIn() { if (m_items == null) { m_items = new List(); 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 selection = new List(); 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().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 { 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); } } }