// // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY // KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR // PURPOSE. // // License: GNU Lesser General Public License (LGPLv3) // // Email: p_torgashov@ukr.net. // // Copyright (C) Pavel Torgashov, 2012-2015. using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Text.RegularExpressions; using System.Windows.Forms; using System.Collections; using System.Diagnostics; namespace AutocompleteMenuNS { [ProvideProperty("AutocompleteMenu", typeof(Control))] public class AutocompleteMenu : Component, IExtenderProvider { private static readonly Dictionary AutocompleteMenuByControls = new Dictionary(); private static readonly Dictionary WrapperByControls = new Dictionary(); private ITextBoxWrapper targetControlWrapper; private readonly Timer timer = new Timer(); private IEnumerable sourceItems = new List(); [Browsable(false)] public IList VisibleItems { get { return Host.ListView.VisibleItems; } private set { Host.ListView.VisibleItems = value;} } private Size maximumSize; /// /// Duration (ms) of tooltip showing /// [Description("Duration (ms) of tooltip showing")] [DefaultValue(3000)] public int ToolTipDuration { get { return Host.ListView.ToolTipDuration; } set { Host.ListView.ToolTipDuration = value; } } public AutocompleteMenu() { Host = new AutocompleteMenuHost(this); Host.ListView.ItemSelected += new EventHandler(ListView_ItemSelected); Host.ListView.ItemHovered += new EventHandler(ListView_ItemHovered); VisibleItems = new List(); Enabled = true; AppearInterval = 500; timer.Tick += timer_Tick; MaximumSize = new Size(180, 200); AutoPopup = true; SearchPattern = @"[\w\.]"; MinFragmentLength = 2; } protected override void Dispose(bool disposing) { if (disposing) { timer.Dispose(); Host.Dispose(); } base.Dispose(disposing); } void ListView_ItemSelected(object sender, EventArgs e) { OnSelecting(); } void ListView_ItemHovered(object sender, HoveredEventArgs e) { OnHovered(e); } public void OnHovered(HoveredEventArgs e) { if (Hovered != null) Hovered(this, e); } [Browsable(false)] public int SelectedItemIndex { get { return Host.ListView.SelectedItemIndex; } internal set { Host.ListView.SelectedItemIndex = value; } } internal AutocompleteMenuHost Host { get; set; } /// /// Called when user selected the control and needed wrapper over it. /// You can assign own Wrapper for target control. /// [Description("Called when user selected the control and needed wrapper over it. You can assign own Wrapper for target control.")] public event EventHandler WrapperNeeded; protected void OnWrapperNeeded(WrapperNeededEventArgs args) { if (WrapperNeeded != null) WrapperNeeded(this, args); if (args.Wrapper == null) args.Wrapper = TextBoxWrapper.Create(args.TargetControl); } ITextBoxWrapper CreateWrapper(Control control) { if (WrapperByControls.ContainsKey(control)) return WrapperByControls[control]; var args = new WrapperNeededEventArgs(control); OnWrapperNeeded(args); if (args.Wrapper != null) WrapperByControls[control] = args.Wrapper; return args.Wrapper; } /// /// Current target control wrapper /// [Browsable(false)] public ITextBoxWrapper TargetControlWrapper { get { return targetControlWrapper; } set { targetControlWrapper = value; if (value != null && !WrapperByControls.ContainsKey(value.TargetControl)) { WrapperByControls[value.TargetControl] = value; SetAutocompleteMenu(value.TargetControl, this); } } } /// /// Maximum size of popup menu /// [DefaultValue(typeof(Size), "180, 200")] [Description("Maximum size of popup menu")] public Size MaximumSize { get { return maximumSize; } set { maximumSize = value; (Host.ListView as Control).MaximumSize = maximumSize; (Host.ListView as Control).Size = maximumSize; Host.CalcSize(); } } /// /// Font /// public Font Font { get { return (Host.ListView as Control).Font; } set { (Host.ListView as Control).Font = value; } } /// /// Left padding of text /// [DefaultValue(18)] [Description("Left padding of text")] public int LeftPadding { get { if (Host.ListView is AutocompleteListView) return (Host.ListView as AutocompleteListView).LeftPadding; else return 0; } set { if (Host.ListView is AutocompleteListView) (Host.ListView as AutocompleteListView).LeftPadding = value; } } /// /// Colors of foreground and background /// [Browsable(true)] [Description("Colors of foreground and background.")] [TypeConverter(typeof(ExpandableObjectConverter))] public Colors Colors { get { return (Host.ListView as IAutocompleteListView).Colors; } set { (Host.ListView as IAutocompleteListView).Colors = value; } } /// /// AutocompleteMenu will popup automatically (when user writes text). Otherwise it will popup only programmatically or by Ctrl-Space. /// [DefaultValue(true)] [Description("AutocompleteMenu will popup automatically (when user writes text). Otherwise it will popup only programmatically or by Ctrl-Space.")] public bool AutoPopup { get; set; } /// /// AutocompleteMenu will capture focus when opening. /// [DefaultValue(false)] [Description("AutocompleteMenu will capture focus when opening.")] public bool CaptureFocus { get; set; } /// /// Indicates whether the component should draw right-to-left for RTL languages. /// [DefaultValue(typeof(RightToLeft), "No")] [Description("Indicates whether the component should draw right-to-left for RTL languages.")] public RightToLeft RightToLeft { get { return Host.RightToLeft; } set { Host.RightToLeft = value; } } /// /// Image list /// public ImageList ImageList { get { return Host.ListView.ImageList; } set { Host.ListView.ImageList = value; } } /// /// Fragment /// [Browsable(false)] public Range Fragment { get; internal set; } /// /// Regex pattern for serach fragment around caret /// [Description("Regex pattern for serach fragment around caret")] [DefaultValue(@"[\w\.]")] public string SearchPattern { get; set; } /// /// Minimum fragment length for popup /// [Description("Minimum fragment length for popup")] [DefaultValue(2)] public int MinFragmentLength { get; set; } /// /// Allows TAB for select menu item /// [Description("Allows TAB for select menu item")] [DefaultValue(false)] public bool AllowsTabKey { get; set; } /// /// Interval of menu appear (ms) /// [Description("Interval of menu appear (ms)")] [DefaultValue(500)] public int AppearInterval { get; set; } [DefaultValue(null)] public string[] Items { get { if (sourceItems == null) return null; var list = new List(); foreach (AutocompleteItem item in sourceItems) list.Add(item.ToString()); return list.ToArray(); } set { SetAutocompleteItems(value); } } /// /// The control for menu displaying. /// Set to null for restore default ListView (AutocompleteListView). /// [Browsable(false)] public IAutocompleteListView ListView { get { return Host.ListView; } set { if (ListView != null) { var ctrl = value as Control; value.ImageList = ImageList; ctrl.RightToLeft = RightToLeft; ctrl.Font = Font; ctrl.MaximumSize = MaximumSize; } Host.ListView = value; Host.ListView.ItemSelected += new EventHandler(ListView_ItemSelected); Host.ListView.ItemHovered += new EventHandler(ListView_ItemHovered); } } [DefaultValue(true)] public bool Enabled { get; set; } /// /// Updates size of the menu /// public void Update() { Host.CalcSize(); } /// /// Returns rectangle of item /// public Rectangle GetItemRectangle(int itemIndex) { return Host.ListView.GetItemRectangle(itemIndex); } #region IExtenderProvider Members bool IExtenderProvider.CanExtend(object extendee) { //find AutocompleteMenu with lowest hashcode if (Container != null) foreach (object comp in Container.Components) if (comp is AutocompleteMenu) if (comp.GetHashCode() < GetHashCode()) return false; //we are main autocomplete menu on form ... //check extendee as TextBox if (!(extendee is Control)) return false; var temp = TextBoxWrapper.Create(extendee as Control); return temp!=null; } public void SetAutocompleteMenu(Control control, AutocompleteMenu menu) { if (menu != null) { if (WrapperByControls.ContainsKey(control)) { var wrapper = WrapperByControls[control]; if (wrapper == null) return; // if (control.IsHandleCreated) menu.SubscribeForm(wrapper); else control.HandleCreated += (o, e) => menu.SubscribeForm(wrapper); // AutocompleteMenuByControls[control] = this; // wrapper.LostFocus += menu.control_LostFocus; wrapper.Scroll += menu.control_Scroll; wrapper.KeyDown += menu.control_KeyDown; wrapper.MouseDown += menu.control_MouseDown; } else { var wrapper = menu.CreateWrapper(control); if (wrapper == null) return; // if (control.IsHandleCreated) menu.SubscribeForm(wrapper); else control.HandleCreated += (o, e) => menu.SubscribeForm(wrapper); // AutocompleteMenuByControls[control] = this; // wrapper.LostFocus += menu.control_LostFocus; wrapper.Scroll += menu.control_Scroll; wrapper.KeyDown += menu.control_KeyDown; wrapper.MouseDown += menu.control_MouseDown; } } else { AutocompleteMenuByControls.TryGetValue(control, out menu); AutocompleteMenuByControls.Remove(control); ITextBoxWrapper wrapper = null; WrapperByControls.TryGetValue(control, out wrapper); WrapperByControls.Remove(control); if (wrapper != null && menu != null) { wrapper.LostFocus -= menu.control_LostFocus; wrapper.Scroll -= menu.control_Scroll; wrapper.KeyDown -= menu.control_KeyDown; wrapper.MouseDown -= menu.control_MouseDown; } } } #endregion /// /// User selects item /// [Description("Occurs when user selects item.")] public event EventHandler Selecting; /// /// It fires after item was inserting /// [Description("Occurs after user selected item.")] public event EventHandler Selected; /// /// It fires when item was hovered /// [Description("Occurs when user hovered item.")] public event EventHandler Hovered; /// /// Occurs when popup menu is opening /// public event EventHandler Opening; private void timer_Tick(object sender, EventArgs e) { timer.Stop(); if(TargetControlWrapper!=null) ShowAutocomplete(false); } private Form myForm; void SubscribeForm(ITextBoxWrapper wrapper) { if (wrapper == null) return; var form = wrapper.TargetControl.FindForm(); if (form == null) return; if (myForm != null) { if (myForm == form) return; UnsubscribeForm(wrapper); } myForm = form; form.LocationChanged += new EventHandler(form_LocationChanged); form.ResizeBegin += new EventHandler(form_LocationChanged); form.FormClosing += new FormClosingEventHandler(form_FormClosing); form.LostFocus += new EventHandler(form_LocationChanged); } void UnsubscribeForm(ITextBoxWrapper wrapper) { if (wrapper == null) return; var form = wrapper.TargetControl.FindForm(); if (form == null) return; form.LocationChanged -= new EventHandler(form_LocationChanged); form.ResizeBegin -= new EventHandler(form_LocationChanged); form.FormClosing -= new FormClosingEventHandler(form_FormClosing); form.LostFocus -= new EventHandler(form_LocationChanged); } private void form_FormClosing(object sender, FormClosingEventArgs e) { Close(); } private void form_LocationChanged(object sender, EventArgs e) { Close(); } private void control_MouseDown(object sender, MouseEventArgs e) { Close(); } ITextBoxWrapper FindWrapper(Control sender) { while (sender != null) { if (WrapperByControls.ContainsKey(sender)) return WrapperByControls[sender]; sender = sender.Parent; } return null; } private void control_KeyDown(object sender, KeyEventArgs e) { TargetControlWrapper = FindWrapper(sender as Control); bool backspaceORdel = e.KeyCode == Keys.Back || e.KeyCode == Keys.Delete; if (Host.Visible) { if (ProcessKey((char)e.KeyCode, Control.ModifierKeys)) e.SuppressKeyPress = true; else if (!backspaceORdel) ResetTimer(1); else ResetTimer(); return; } if (!Host.Visible) { switch (e.KeyCode) { case Keys.Up: case Keys.Down: case Keys.PageUp: case Keys.PageDown: case Keys.Left: case Keys.Right: case Keys.End: case Keys.Home: case Keys.ControlKey: case Keys.Escape: case Keys.Tab: { timer.Stop(); return; } } if (Control.ModifierKeys== Keys.Alt) { timer.Stop(); return; } if (Control.ModifierKeys == Keys.Control && e.KeyCode == Keys.Space) { ShowAutocomplete(true); e.SuppressKeyPress = true; return; } } ResetTimer(); } void ResetTimer() { ResetTimer(-1); } void ResetTimer(int interval) { if (interval <= 0) timer.Interval = AppearInterval; else timer.Interval = interval; timer.Stop(); timer.Start(); } private void control_Scroll(object sender, ScrollEventArgs e) { Close(); } private void control_LostFocus(object sender, EventArgs e) { if (!Host.Focused) Close(); } public AutocompleteMenu GetAutocompleteMenu(Control control) { if (AutocompleteMenuByControls.ContainsKey(control)) return AutocompleteMenuByControls[control]; else return null; } bool forcedOpened = false; internal void ShowAutocomplete(bool forced) { if (forced) forcedOpened = true; if (TargetControlWrapper != null && TargetControlWrapper.Readonly) { Close(); return; } if (!Enabled) { Close(); return; } if (!forcedOpened && !AutoPopup) { Close(); return; } //build list BuildAutocompleteList(forcedOpened); //show popup menu if (VisibleItems.Count > 0) { if (forced && VisibleItems.Count == 1 && Host.ListView.SelectedItemIndex == 0) { //do autocomplete if menu contains only one line and user press CTRL-SPACE OnSelecting(); Close(); } else ShowMenu(); } else Close(); } private void ShowMenu() { if (!Host.Visible) { var args = new CancelEventArgs(); OnOpening(args); if (!args.Cancel) { //calc screen point for popup menu Point point = TargetControlWrapper.TargetControl.Location; point.Offset(2, TargetControlWrapper.TargetControl.Height + 2); point = TargetControlWrapper.GetPositionFromCharIndex(Fragment.Start); point.Offset(2, TargetControlWrapper.TargetControl.Font.Height + 2); // Host.Show(TargetControlWrapper.TargetControl, point); if (CaptureFocus) { (Host.ListView as Control).Focus(); //ProcessKey((char) Keys.Down, Keys.None); } } } else (Host.ListView as Control).Invalidate(); } private void BuildAutocompleteList(bool forced) { var visibleItems = new List(); bool foundSelected = false; int selectedIndex = -1; //get fragment around caret Range fragment = GetFragment(SearchPattern); string text = fragment.Text; // if (sourceItems != null) if (forced || (text.Length >= MinFragmentLength /* && tb.Selection.Start == tb.Selection.End*/)) { Fragment = fragment; //build popup menu foreach (AutocompleteItem item in sourceItems) { item.Parent = this; CompareResult res = item.Compare(text); if (res != CompareResult.Hidden) visibleItems.Add(item); if (res == CompareResult.VisibleAndSelected && !foundSelected) { foundSelected = true; selectedIndex = visibleItems.Count - 1; } } } VisibleItems = visibleItems; if (foundSelected) Host.ListView.SelectedItemIndex = selectedIndex; else Host.ListView.SelectedItemIndex = 0; Host.ListView.HighlightedItemIndex = -1; Host.CalcSize(); } internal void OnOpening(CancelEventArgs args) { if (Opening != null) Opening(this, args); } private Range GetFragment(string searchPattern) { var tb = TargetControlWrapper; if (tb.SelectionLength > 0) return new Range(tb); string text = tb.Text; var regex = new Regex(searchPattern); var result = new Range(tb); int startPos = tb.SelectionStart; //go forward int i = startPos; while (i >= 0 && i < text.Length) { if (!regex.IsMatch(text[i].ToString())) break; i++; } result.End = i; //go backward i = startPos; while (i > 0 && (i - 1) < text.Length) { if (!regex.IsMatch(text[i - 1].ToString())) break; i--; } result.Start = i; return result; } public void Close() { Host.ListView.HideToolTip(Host.ListView.GetParentControl()); Host.Close(); forcedOpened = false; } public void SetAutocompleteItems(IEnumerable items) { var list = new List(); if (items == null) { sourceItems = null; return; } foreach (string item in items) list.Add(new AutocompleteItem(item)); SetAutocompleteItems(list); } public void SetAutocompleteItems(IEnumerable items) { sourceItems = items; } public void AddItem(string item) { AddItem(new AutocompleteItem(item)); } public void AddItem(AutocompleteItem item) { if (sourceItems == null) sourceItems = new List(); if (sourceItems is IList) (sourceItems as IList).Add(item); else throw new Exception("Current autocomplete items does not support adding"); } /// /// Shows popup menu immediately /// /// If True - MinFragmentLength will be ignored public void Show(Control control, bool forced) { SetAutocompleteMenu(control, this); this.TargetControlWrapper = FindWrapper(control); ShowAutocomplete(forced); } internal virtual void OnSelecting() { if (SelectedItemIndex < 0 || SelectedItemIndex >= VisibleItems.Count) return; AutocompleteItem item = VisibleItems[SelectedItemIndex]; var args = new SelectingEventArgs { Item = item, SelectedIndex = SelectedItemIndex }; OnSelecting(args); if (args.Cancel) { SelectedItemIndex = args.SelectedIndex; (Host.ListView as Control).Invalidate(true); return; } if (!args.Handled) { Range fragment = Fragment; ApplyAutocomplete(item, fragment); } Close(); // var args2 = new SelectedEventArgs { Item = item, Control = TargetControlWrapper.TargetControl }; item.OnSelected(args2); OnSelected(args2); } private void ApplyAutocomplete(AutocompleteItem item, Range fragment) { string newText = item.GetTextForReplace(); //replace text of fragment fragment.Text = newText; fragment.TargetWrapper.TargetControl.Focus(); } internal void OnSelecting(SelectingEventArgs args) { if (Selecting != null) Selecting(this, args); } public void OnSelected(SelectedEventArgs args) { if (Selected != null) Selected(this, args); } public void SetColumns(string[] columns, int[] columnsWidth = null) { ListView.ColumnsTitle = columns; ListView.ColumnsWidth = columnsWidth; } public void SelectNext(int shift) { SelectedItemIndex = Math.Max(0, Math.Min(SelectedItemIndex + shift, VisibleItems.Count - 1)); // (Host.ListView as Control).Invalidate(); } public bool ProcessKey(char c, Keys keyModifiers) { var page = Host.Height / (Font.Height + 4); if (keyModifiers == Keys.None) switch ((Keys) c) { case Keys.Down: SelectNext(+1); return true; case Keys.PageDown: SelectNext(+page); return true; case Keys.Up: SelectNext(-1); return true; case Keys.PageUp: SelectNext(-page); return true; case Keys.Enter: OnSelecting(); return true; case Keys.Tab: if (!AllowsTabKey) break; OnSelecting(); return true; case Keys.Left: case Keys.Right: Close(); return false; case Keys.Escape: Close(); return true; } return false; } /// /// Menu is visible /// public bool Visible { get { return Host != null && Host.Visible; } } } }