using System.ComponentModel; using System.Drawing.Drawing2D; using System.Drawing.Text; using System.Text.RegularExpressions; namespace LB_VisionControls { /// /// Popup menu for autocomplete /// [Browsable(false)] public class AutocompleteMenu : ToolStripDropDown { AutocompleteListView listView; public ToolStripControlHost host; public Range Fragment { get; internal set; } /// /// Regex pattern for serach fragment around caret /// public string SearchPattern { get; set; } /// /// Minimum fragment length for popup /// public int MinFragmentLength { get; set; } /// /// User selects item /// public event EventHandler Selecting; /// /// It fires after item inserting /// public event EventHandler Selected; /// /// Occurs when popup menu is opening /// public event EventHandler Opening; /// /// Allow TAB for select menu item /// public bool AllowTabKey { get { return listView.AllowTabKey; } set { listView.AllowTabKey = value; } } /// /// Interval of menu appear (ms) /// public int AppearInterval { get { return listView.AppearInterval; } set { listView.AppearInterval = value; } } protected override void OnPaint(PaintEventArgs e) { e.Graphics.SmoothingMode = SmoothingMode.HighQuality; e.Graphics.FillRectangle(new SolidBrush(Color.FromArgb(50, 37, 38)), new Rectangle(-1, -1, this.ClientRectangle.Width + 2, this.ClientRectangle.Height + 2)); //base.OnPaint(e); } public AutocompleteMenu(FastColoredTextBox tb) { // create a new popup and add the list view to it AutoClose = false; AutoSize = false; Margin = Padding.Empty; Padding = Padding.Empty; listView = new AutocompleteListView(tb); host = new ToolStripControlHost(listView); host.Margin = new Padding(1, 1, 1, 1); host.Padding = Padding.Empty; host.AutoSize = false; host.AutoToolTip = false; CalcSize(); base.Items.Add(host); listView.Parent = this; SearchPattern = @"[\w\.]"; MinFragmentLength = 2; } internal void OnOpening(CancelEventArgs args) { if (Opening != null) Opening(this, args); } public new void Close() { listView.toolTip.Hide(listView); base.Close(); } internal void CalcSize() { host.Size = listView.Size; Size = new System.Drawing.Size(listView.Size.Width, listView.Size.Height); } public virtual void OnSelecting() { listView.OnSelecting(); } public void SelectNext(int shift) { listView.SelectNext(shift); } internal void OnSelecting(SelectingEventArgs args) { if (Selecting != null) Selecting(this, args); } public void OnSelected(SelectedEventArgs args) { if (Selected != null) Selected(this, args); } public new AutocompleteListView Items { get { return listView; } } /// /// Shows popup menu immediately /// /// If True - MinFragmentLength will be ignored public void Show(bool forced) { Items.DoAutocomplete(forced); } } public class AutocompleteListView : UserControl { internal List visibleItems; IEnumerable sourceItems = new List(); int selectedItemIndex = 0; int hoveredItemIndex = -1; int itemHeight; AutocompleteMenu Menu { get { return Parent as AutocompleteMenu; } } int oldItemCount = 0; FastColoredTextBox tb; internal ToolTip toolTip = new ToolTip(); System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer(); internal bool AllowTabKey { get; set; } public ImageList ImageList { get; set; } internal int AppearInterval { get { return timer.Interval; } set { timer.Interval = value; } } internal AutocompleteListView(FastColoredTextBox tb) { this.Width = 210; SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true); base.Font = new Font(FontFamily.GenericSansSerif, 9); visibleItems = new List(); itemHeight = Font.Height + 2; VerticalScroll.SmallChange = itemHeight; this.BackColor = Color.Transparent; //Color.FromArgb(255, 255, 17, 18); BorderStyle = BorderStyle.None; MaximumSize = new Size(Size.Width, 180); toolTip.ShowAlways = false; AppearInterval = 500; timer.Tick += new EventHandler(timer_Tick); this.tb = tb; tb.KeyDown += new KeyEventHandler(tb_KeyDown); tb.SelectionChanged += new EventHandler(tb_SelectionChanged); tb.KeyPressed += new KeyPressEventHandler(tb_KeyPressed); Form form = tb.FindForm(); if (form != null) { form.LocationChanged += (o, e) => Menu.Close(); form.ResizeBegin += (o, e) => Menu.Close(); form.FormClosing += (o, e) => Menu.Close(); form.LostFocus += (o, e) => Menu.Close(); } tb.LostFocus += (o, e) => { if (!Menu.Focused) Menu.Close(); }; tb.Scroll += (o, e) => Menu.Close(); } void tb_KeyPressed(object sender, KeyPressEventArgs e) { bool backspaceORdel = e.KeyChar == '\b' || e.KeyChar == 0xff; /* if (backspaceORdel) prevSelection = tb.Selection.Start;*/ if (Menu.Visible && !backspaceORdel) DoAutocomplete(false); else ResetTimer(timer); } void timer_Tick(object sender, EventArgs e) { timer.Stop(); DoAutocomplete(false); } void ResetTimer(System.Windows.Forms.Timer timer) { timer.Stop(); timer.Start(); } internal void DoAutocomplete() { DoAutocomplete(false); } internal void DoAutocomplete(bool forced) { if (!Menu.Enabled) { Menu.Close(); return; } visibleItems.Clear(); selectedItemIndex = 0; VerticalScroll.Value = 0; //get fragment around caret Range fragment = tb.Selection.GetFragment(Menu.SearchPattern); string text = fragment.Text; //calc screen point for popup menu Point point = tb.PlaceToPoint(fragment.End); point.Offset(2, tb.CharHeight); // if (forced || (text.Length >= Menu.MinFragmentLength && tb.Selection.IsEmpty)) { Menu.Fragment = fragment; bool foundSelected = false; //build popup menu foreach (var item in sourceItems) { item.Parent = Menu; CompareResult res = item.Compare(text); if (res != CompareResult.Hidden) visibleItems.Add(item); if (res == CompareResult.VisibleAndSelected && !foundSelected) { foundSelected = true; selectedItemIndex = visibleItems.Count - 1; } } if (foundSelected) { AdjustScroll(); DoSelectedVisible(); } } //show popup menu if (Count > 0) { if (!Menu.Visible) { CancelEventArgs args = new CancelEventArgs(); Menu.OnOpening(args); if (!args.Cancel) Menu.Show(tb, point); } else Invalidate(); } else Menu.Close(); } void tb_SelectionChanged(object sender, EventArgs e) { /* FastColoredTextBox tb = sender as FastColoredTextBox; if (Math.Abs(prevSelection.iChar - tb.Selection.Start.iChar) > 1 || prevSelection.iLine != tb.Selection.Start.iLine) Menu.Close(); prevSelection = tb.Selection.Start;*/ if (Menu.Visible) { bool needClose = false; if (!tb.Selection.IsEmpty) needClose = true; else if (!Menu.Fragment.Contains(tb.Selection.Start)) { if (tb.Selection.Start.iLine == Menu.Fragment.End.iLine && tb.Selection.Start.iChar == Menu.Fragment.End.iChar + 1) { //user press key at end of fragment char c = tb.Selection.CharBeforeStart; if (!Regex.IsMatch(c.ToString(), Menu.SearchPattern))//check char needClose = true; } else needClose = true; } if (needClose) Menu.Close(); } } void tb_KeyDown(object sender, KeyEventArgs e) { if (Menu.Visible) if (ProcessKey(e.KeyCode, e.Modifiers)) e.Handled = true; if (!Menu.Visible) if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Space) { DoAutocomplete(); e.Handled = true; } } void AdjustScroll() { if (oldItemCount == visibleItems.Count) return; int needHeight = itemHeight * visibleItems.Count + 1; Height = Math.Min(needHeight, MaximumSize.Height); Menu.CalcSize(); AutoScrollMinSize = new Size(0, needHeight); oldItemCount = visibleItems.Count; } protected override void OnPaint(PaintEventArgs e) { AdjustScroll(); e.Graphics.SmoothingMode = SmoothingMode.HighQuality; e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias; int startI = VerticalScroll.Value / itemHeight - 1; int finishI = (VerticalScroll.Value + ClientSize.Height) / itemHeight + 1; startI = Math.Max(startI, 0); finishI = Math.Min(finishI, visibleItems.Count); int y = 0; int leftPadding = 20; for (int i = startI; i < finishI; i++) { y = i * itemHeight - VerticalScroll.Value; //if (ImageList != null && visibleItems[i].ImageIndex >= 0) // e.Graphics.DrawImage(ImageList.Images[visibleItems[i].ImageIndex], 1, y); if (i == selectedItemIndex) { Brush selectedBrush = new SolidBrush(Color.Yellow);//new LinearGradientBrush(new Point(0, y - 3), new Point(0, y + itemHeight), Color.GreenYellow, Color.Green); e.Graphics.FillRectangle(selectedBrush, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight - 1); e.Graphics.DrawRectangle(Pens.Yellow, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight - 1); e.Graphics.DrawString(visibleItems[i].ToString(), Font, Brushes.Black, leftPadding, y); } else { e.Graphics.DrawString(visibleItems[i].ToString(), Font, Brushes.LightSkyBlue, leftPadding, y); } if (i == hoveredItemIndex) e.Graphics.DrawRectangle(Pens.Red, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight - 1); } } protected override void OnScroll(ScrollEventArgs se) { base.OnScroll(se); Invalidate(); } protected override void OnMouseClick(MouseEventArgs e) { base.OnMouseClick(e); if (e.Button == System.Windows.Forms.MouseButtons.Left) { selectedItemIndex = PointToItemIndex(e.Location); DoSelectedVisible(); Invalidate(); } } protected override void OnMouseDoubleClick(MouseEventArgs e) { base.OnMouseDoubleClick(e); selectedItemIndex = PointToItemIndex(e.Location); Invalidate(); OnSelecting(); } internal virtual void OnSelecting() { if (selectedItemIndex < 0 || selectedItemIndex >= visibleItems.Count) return; tb.TextSource.Manager.BeginAutoUndoCommands(); try { AutocompleteItem item = visibleItems[selectedItemIndex]; SelectingEventArgs args = new SelectingEventArgs() { Item = item, SelectedIndex = selectedItemIndex }; Menu.OnSelecting(args); if (args.Cancel) { selectedItemIndex = args.SelectedIndex; Invalidate(); return; } if (!args.Handled) { var fragment = Menu.Fragment; DoAutocomplete(item, fragment); } Menu.Close(); // SelectedEventArgs args2 = new SelectedEventArgs() { Item = item, Tb = Menu.Fragment.tb }; item.OnSelected(Menu, args2); Menu.OnSelected(args2); } finally { tb.TextSource.Manager.EndAutoUndoCommands(); } } private void DoAutocomplete(AutocompleteItem item, Range fragment) { string newText = item.GetTextForReplace(); //replace text of fragment var tb = fragment.tb; if (tb.Selection.ColumnSelectionMode) { var start = tb.Selection.Start; var end = tb.Selection.End; start.iChar = fragment.Start.iChar; end.iChar = fragment.End.iChar; tb.Selection.Start = start; tb.Selection.End = end; } else { tb.Selection.Start = fragment.Start; tb.Selection.End = fragment.End; } tb.InsertText(newText); tb.Focus(); } int PointToItemIndex(Point p) { return (p.Y + VerticalScroll.Value) / itemHeight; } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { ProcessKey(keyData, Keys.None); return base.ProcessCmdKey(ref msg, keyData); } private bool ProcessKey(Keys keyData, Keys keyModifiers) { if (keyModifiers == Keys.None) switch (keyData) { case Keys.Down: SelectNext(+1); return true; case Keys.PageDown: SelectNext(+10); return true; case Keys.Up: SelectNext(-1); return true; case Keys.PageUp: SelectNext(-10); return true; case Keys.Enter: case Keys.Space: OnSelecting(); return true; case Keys.Tab: if (!AllowTabKey) break; OnSelecting(); return true; case Keys.Escape: Menu.Close(); return true; } return false; } public void SelectNext(int shift) { selectedItemIndex = Math.Max(0, Math.Min(selectedItemIndex + shift, visibleItems.Count - 1)); DoSelectedVisible(); // Invalidate(); } private void DoSelectedVisible() { if (selectedItemIndex >= 0 && selectedItemIndex < visibleItems.Count) SetToolTip(visibleItems[selectedItemIndex]); var y = selectedItemIndex * itemHeight - VerticalScroll.Value; if (y < 0) VerticalScroll.Value = selectedItemIndex * itemHeight; if (y > ClientSize.Height - itemHeight) VerticalScroll.Value = Math.Min(VerticalScroll.Maximum, selectedItemIndex * itemHeight - ClientSize.Height + itemHeight); //some magic for update scrolls AutoScrollMinSize -= new Size(1, 0); AutoScrollMinSize += new Size(1, 0); } private void SetToolTip(AutocompleteItem autocompleteItem) { var title = visibleItems[selectedItemIndex].ToolTipTitle; var text = visibleItems[selectedItemIndex].ToolTipText; if (string.IsNullOrEmpty(title)) { toolTip.ToolTipTitle = null; toolTip.SetToolTip(this, null); return; } if (string.IsNullOrEmpty(text)) { toolTip.ToolTipTitle = null; toolTip.Show(title, this, Width + 3, 0, 3000); } else { toolTip.ToolTipTitle = title; toolTip.Show(text, this, Width + 3, 0, 3000); } } public int Count { get { return visibleItems.Count; } } public void SetAutocompleteItems(ICollection items) { List list = new List(items.Count); foreach (var item in items) list.Add(new AutocompleteItem(item)); SetAutocompleteItems(list); } public void SetAutocompleteItems(ICollection items) { sourceItems = items; } } public class SelectingEventArgs : EventArgs { public AutocompleteItem Item { get; internal set; } public bool Cancel { get; set; } public int SelectedIndex { get; set; } public bool Handled { get; set; } } public class SelectedEventArgs : EventArgs { public AutocompleteItem Item { get; internal set; } public FastColoredTextBox Tb { get; set; } } }