Mesen-X/GUI.NET/Debugger/FastColoredTextBox/AutocompleteMenu.cs
2017-08-30 18:31:27 -04:00

750 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;
using System.Drawing.Drawing2D;
using System.Text.RegularExpressions;
namespace FastColoredTextBoxNS
{
/// <summary>
/// Popup menu for autocomplete
/// </summary>
[Browsable(false)]
public class AutocompleteMenu : UserControl
{
AutocompleteListView listView;
public Range Fragment { get; internal set; }
/// <summary>
/// Regex pattern for serach fragment around caret
/// </summary>
public string SearchPattern { get; set; }
/// <summary>
/// Minimum fragment length for popup
/// </summary>
public int MinFragmentLength { get; set; }
/// <summary>
/// User selects item
/// </summary>
public event EventHandler<SelectingEventArgs> Selecting;
/// <summary>
/// It fires after item inserting
/// </summary>
public event EventHandler<SelectedEventArgs> Selected;
/// <summary>
/// Occurs when popup menu is opening
/// </summary>
public event EventHandler<CancelEventArgs> Opening;
/// <summary>
/// Allow TAB for select menu item
/// </summary>
public bool AllowTabKey { get { return listView.AllowTabKey; } set { listView.AllowTabKey = value; } }
/// <summary>
/// Interval of menu appear (ms)
/// </summary>
public int AppearInterval { get { return listView.AppearInterval; } set { listView.AppearInterval = value; } }
/// <summary>
/// Back color of selected item
/// </summary>
[DefaultValue(typeof(Color), "Orange")]
public Color SelectedColor
{
get { return listView.SelectedColor; }
set { listView.SelectedColor = value; }
}
/// <summary>
/// Border color of hovered item
/// </summary>
[DefaultValue(typeof(Color), "Red")]
public Color HoveredColor
{
get { return listView.HoveredColor; }
set { listView.HoveredColor = value; }
}
public AutocompleteMenu(FastColoredTextBox tb, Form parentForm)
{
// create a new popup and add the list view to it
Visible = false;
BorderStyle = BorderStyle.FixedSingle;
AutoSize = false;
Margin = Padding.Empty;
Padding = Padding.Empty;
BackColor = Color.White;
listView = new AutocompleteListView(this, tb, parentForm);
CalcSize();
this.Controls.Add(listView);
SearchPattern = @"[\w\.]";
MinFragmentLength = 2;
}
public new Font Font
{
get { return listView.Font; }
set { listView.Font = value; }
}
internal void OnOpening(CancelEventArgs args)
{
if (Opening != null)
Opening(this, args);
}
public void Close()
{
listView.toolTip.Hide(listView);
this.Hide();
}
internal void CalcSize()
{
Size = new System.Drawing.Size(listView.Size.Width + 2, listView.Size.Height + 2);
}
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 AutocompleteListView Items
{
get { return listView; }
}
/// <summary>
/// Shows popup menu immediately
/// </summary>
/// <param name="forced">If True - MinFragmentLength will be ignored</param>
public void Show(bool forced)
{
Items.DoAutocomplete(forced);
}
/// <summary>
/// Minimal size of menu
/// </summary>
public new Size MinimumSize
{
get { return Items.MinimumSize; }
set { Items.MinimumSize = value; }
}
/// <summary>
/// Image list of menu
/// </summary>
public ImageList ImageList
{
get { return Items.ImageList; }
set { Items.ImageList = value; }
}
/// <summary>
/// Tooltip duration (ms)
/// </summary>
public int ToolTipDuration
{
get { return Items.ToolTipDuration; }
set { Items.ToolTipDuration = value; }
}
/// <summary>
/// Tooltip
/// </summary>
public ToolTip ToolTip
{
get { return Items.toolTip; }
set { Items.toolTip = value; }
}
}
[System.ComponentModel.ToolboxItem(false)]
public class AutocompleteListView : UserControl
{
public event EventHandler FocussedItemIndexChanged;
internal List<AutocompleteItem> visibleItems;
IEnumerable<AutocompleteItem> sourceItems = new List<AutocompleteItem>();
int focussedItemIndex = 0;
int hoveredItemIndex = -1;
private int ItemHeight
{
get { return Font.Height + 2; }
}
private AutocompleteMenu Menu { get; set; }
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 int ToolTipDuration { get; set; }
public override Size GetPreferredSize(Size proposedSize)
{
return Size;
}
public Color SelectedColor { get; set; }
public Color HoveredColor { get; set; }
public int FocussedItemIndex
{
get { return focussedItemIndex; }
set
{
if (focussedItemIndex != value)
{
focussedItemIndex = value;
if (FocussedItemIndexChanged != null)
FocussedItemIndexChanged(this, EventArgs.Empty);
}
}
}
public AutocompleteItem FocussedItem
{
get
{
if (FocussedItemIndex >= 0 && focussedItemIndex < visibleItems.Count)
return visibleItems[focussedItemIndex];
return null;
}
set
{
FocussedItemIndex = visibleItems.IndexOf(value);
}
}
public Form AutocompleteParent { get; set; }
internal AutocompleteListView(AutocompleteMenu menu, FastColoredTextBox tb, Form parent)
{
Menu = menu;
AutocompleteParent = parent;
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.UserPaint, true);
base.Font = new Font(FontFamily.GenericSansSerif, 9);
visibleItems = new List<AutocompleteItem>();
VerticalScroll.SmallChange = ItemHeight;
MaximumSize = new Size(Size.Width, 180);
toolTip.ShowAlways = false;
AppearInterval = 200;
timer.Tick += new EventHandler(timer_Tick);
SelectedColor = Color.Orange;
HoveredColor = Color.Red;
ToolTipDuration = 30000;
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();
this.VisibleChanged += (o, e) =>
{
if (this.Visible)
DoSelectedVisible();
};
}
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();
FocussedItemIndex = 0;
VerticalScroll.Value = 0;
//some magic for update scrolls
AutoScrollMinSize += new Size(1, 0);
AutoScrollMinSize -= new Size(1, 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 = tb.PointToScreen(point);
point = AutocompleteParent.PointToClient(offset);
point.Offset(2, tb.CharHeight);
//
if (forced || (text.Length >= Menu.MinFragmentLength
&& tb.Selection.IsEmpty /*pops up only if selected range is empty*/
&& (tb.Selection.Start > fragment.Start || text.Length == 0/*pops up only if caret is after first letter*/)))
{
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;
FocussedItemIndex = 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.Location = point;
Menu.Parent = AutocompleteParent;
Menu.Show();
Menu.BringToFront();
}
}
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)
{
var tb = sender as FastColoredTextBox;
if (Menu.Visible)
if (ProcessKey(e.KeyCode, e.Modifiers))
e.Handled = true;
if (!Menu.Visible)
{
if (tb.HotkeysMapping.ContainsKey(e.KeyData) && tb.HotkeysMapping[e.KeyData] == FCTBAction.AutocompleteMenu)
{
DoAutocomplete();
e.Handled = true;
}
else
{
if (e.KeyCode == Keys.Escape && timer.Enabled)
timer.Stop();
}
}
}
void AdjustScroll()
{
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 = tb.PointToScreen(point);
point = AutocompleteParent.PointToClient(offset);
point.Offset(2, tb.CharHeight);
if(Menu.Width + point.X > AutocompleteParent.ClientSize.Width - 10) {
point.X -= Menu.Width;
}
if(Menu.Height + point.Y > AutocompleteParent.ClientSize.Height - 10) {
point.Y -= Menu.Height + 15;
}
Menu.Location = point;
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();
var itemHeight = ItemHeight;
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 = 18;
for (int i = startI; i < finishI; i++)
{
y = i * itemHeight - VerticalScroll.Value;
var item = visibleItems[i];
if(item.BackColor != Color.Transparent)
using (var brush = new SolidBrush(item.BackColor))
e.Graphics.FillRectangle(brush, 1, y, ClientSize.Width, itemHeight);
if (ImageList != null && visibleItems[i].ImageIndex >= 0)
e.Graphics.DrawImage(ImageList.Images[item.ImageIndex], 1, y);
if (i == FocussedItemIndex)
using (var selectedBrush = new LinearGradientBrush(new Point(0, y - 3), new Point(0, y + itemHeight), Color.Transparent, SelectedColor))
using (var pen = new Pen(SelectedColor))
{
e.Graphics.FillRectangle(selectedBrush, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight);
e.Graphics.DrawRectangle(pen, leftPadding, y, ClientSize.Width - 1 - leftPadding, itemHeight);
}
if (i == hoveredItemIndex)
using(var pen = new Pen(HoveredColor))
e.Graphics.DrawRectangle(pen, leftPadding, y, ClientSize.Width - leftPadding, itemHeight);
using (var brush = new SolidBrush(item.ForeColor != Color.Transparent ? item.ForeColor : ForeColor))
e.Graphics.DrawString(item.ToString(), Font, brush, leftPadding, y);
}
}
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)
{
FocussedItemIndex = PointToItemIndex(e.Location);
DoSelectedVisible();
Invalidate();
}
}
protected override void OnMouseDoubleClick(MouseEventArgs e)
{
base.OnMouseDoubleClick(e);
FocussedItemIndex = PointToItemIndex(e.Location);
Invalidate();
OnSelecting();
}
internal virtual void OnSelecting()
{
if (FocussedItemIndex < 0 || FocussedItemIndex >= visibleItems.Count)
return;
tb.TextSource.Manager.BeginAutoUndoCommands();
try
{
AutocompleteItem item = FocussedItem;
SelectingEventArgs args = new SelectingEventArgs()
{
Item = item,
SelectedIndex = FocussedItemIndex
};
Menu.OnSelecting(args);
if (args.Cancel)
{
FocussedItemIndex = 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;
tb.BeginAutoUndo();
tb.TextSource.Manager.ExecuteCommand(new SelectCommand(tb.TextSource));
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.TextSource.Manager.ExecuteCommand(new SelectCommand(tb.TextSource));
tb.EndAutoUndo();
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:
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)
{
FocussedItemIndex = Math.Max(0, Math.Min(FocussedItemIndex + shift, visibleItems.Count - 1));
DoSelectedVisible();
//
Invalidate();
}
private void DoSelectedVisible()
{
if (FocussedItem != null)
SetToolTip(FocussedItem);
var y = FocussedItemIndex * ItemHeight - VerticalScroll.Value;
if (y < 0)
VerticalScroll.Value = FocussedItemIndex * ItemHeight;
if (y > ClientSize.Height - ItemHeight)
VerticalScroll.Value = Math.Min(VerticalScroll.Maximum, FocussedItemIndex * 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 = autocompleteItem.ToolTipTitle;
var text = autocompleteItem.ToolTipText;
Control window = tb;
if (string.IsNullOrEmpty(title))
{
toolTip.Hide(window);
return;
}
var location = new Point(Right + 3 + Menu.Left, Menu.Top);
if (string.IsNullOrEmpty(text))
{
toolTip.ToolTipTitle = null;
if (ToolTipDuration == 0)
toolTip.Show(title, window, location);
else
toolTip.Show(title, window, location, ToolTipDuration);
}
else
{
toolTip.ToolTipTitle = title;
if (ToolTipDuration == 0)
toolTip.Show(text, window, location);
else
toolTip.Show(text, window, location, ToolTipDuration);
}
}
public int Count
{
get { return visibleItems.Count; }
}
public void SetAutocompleteItems(ICollection<string> items)
{
List<AutocompleteItem> list = new List<AutocompleteItem>(items.Count);
foreach (var item in items)
list.Add(new AutocompleteItem(item));
SetAutocompleteItems(list);
}
public void SetAutocompleteItems(IEnumerable<AutocompleteItem> 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; }
}
}