using System; using System.Collections.Generic; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; namespace Common { public partial class RichTextBoxEx : RichTextBox { private bool linksActive; public delegate void LinkClickHandler(string link); public event LinkClickHandler LinkClick; public string Title { get; set; } public Form ParentForm; public List LinkMatches; public FormFind FormFind; public string PreviousRtf; private ToolStripMenuItem tsmiUndo; private ToolStripMenuItem tsmiRedo; private ToolStripMenuItem tsmiCut; private ToolStripMenuItem tsmiCopy; private ToolStripMenuItem tsmiPaste; private ToolStripMenuItem tsmiDelete; private ToolStripMenuItem tsmiSelectAll; private ToolStripMenuItem tsmiFind; private ToolStripMenuItem tsmiShowInWindow; private bool interfaceUpdateEnabled = true; public RichTextBoxEx() { //InitializeComponent(); KeyDown += HandleKeyDown; KeyUp += HandleKeyUp; MouseClick += HandleMouseClick; MouseLeave += HandleMouseLeave; Leave += HandleLeave; SelectionChanged += delegate { if (interfaceUpdateEnabled) UpdateInterface(); }; AddContextMenu(); LinkMatches = new List(); } protected override void OnPaint(PaintEventArgs pe) { base.OnPaint(pe); } private void ShowRichTextBoxLinks(RichTextBox richTextBox) { bool lastReadOnlyState = ReadOnly; ReadOnly = false; interfaceUpdateEnabled = false; PreviousRtf = Rtf; RichTextBoxContext context = new RichTextBoxContext(); context.SaveContext(this); List linkMatchStart = new List(); foreach (var linkMatch in LinkMatches) { linkMatchStart.Add(-2); } List linkMatchStartString = new List(); foreach (var linkMatch in LinkMatches) linkMatchStartString.Add(linkMatch.StartString.ToLowerInvariant()); string content = richTextBox.Text; string contentLowerCase = content.ToLowerInvariant(); RichTextBox tempRichTextBox = new RichTextBox(); tempRichTextBox.Rtf = richTextBox.Rtf; int contentStart = 0; do { int firstIndex = content.Length; LinkMatch firstMatch = null; if ((content.Length - contentStart) > 0) { int i = 0; foreach (var linkMatch in LinkMatches) { if ((linkMatchStart[i] < contentStart) && (linkMatchStart[i] != -1)) { if (linkMatch.CaseSensitive) { linkMatchStart[i] = content.IndexOf(linkMatch.StartString, contentStart, StringComparison.Ordinal); } else { linkMatchStart[i] = contentLowerCase.IndexOf(linkMatchStartString[i], contentStart, StringComparison.Ordinal); } } if ((linkMatchStart[i] != -1) && (linkMatchStart[i] < firstIndex)) { firstMatch = linkMatch; firstIndex = linkMatchStart[i]; } i++; } if (firstMatch == null) break; } else break; int index = firstIndex; string startString = firstMatch.StartString; int linkLength = startString.Length; var selectionStart = index; if (firstMatch.ExecuteMatch(content, index + startString.Length, out var linkTextAfter)) { if ((firstMatch.StartString != "") || ((firstMatch.StartString == "") && (linkTextAfter.Length == 5) && ((index < 1) || ((index >= 1) && ((content[index - 1] == ' ') || (content[index - 1] == '\n')))) && ((index + linkTextAfter.Length + 1 >= content.Length) || ((index + linkTextAfter.Length + 1 < content.Length) && ((content[index + linkTextAfter.Length] == ' ') || (content[index + linkTextAfter.Length] == '\r') || (content[index + linkTextAfter.Length] == '\n') || (content[index + linkTextAfter.Length] == ',') || (content[index + linkTextAfter.Length] == ';') || (content[index + linkTextAfter.Length] == '.')))) )) { linkLength += linkTextAfter.Length; string linkText = content.Substring(index, linkLength); tempRichTextBox.SelectionStart = selectionStart; tempRichTextBox.SelectionLength = linkText.Length; tempRichTextBox.SelectionFont = new Font(tempRichTextBox.SelectionFont.FontFamily, tempRichTextBox.SelectionFont.Size, FontStyle.Underline); tempRichTextBox.SelectionColor = Color.LightBlue; } } selectionStart += linkLength; if (linkLength == 0) linkLength = 1; contentStart = index + linkLength; } while (true); //richTextBox.SelectionStart = 0; richTextBox.SelectAll(); richTextBox.SelectedRtf = tempRichTextBox.Rtf; context.LoadContext(this); interfaceUpdateEnabled = true; ReadOnly = lastReadOnlyState; } [DllImport("user32.dll")] private static extern int SendMessage(IntPtr hwndLock, Int32 wMsg, Int32 wParam, Int32 lParam); const int WM_USER = 0x400; const int EM_GETUNDONAME = WM_USER + 86; public enum UndoNameId { Unknown = 0, Typing = 1, Delete = 2, DragDrop = 3, Cut = 4, Paste = 5, AutoTable = 6 }; public UndoNameId UndoActionId { get { if (!CanUndo) return UndoNameId.Unknown; UndoNameId n; n = (UndoNameId)SendMessage(Handle, EM_GETUNDONAME, 0, 0); return n; } } public void UndoUnknownActions() { int i = 0; while (CanUndo && (UndoActionId == UndoNameId.Unknown)) { Undo(); i++; if (i > 1000) break; } } private void HideRichTextBoxLinks() { bool lastReadOnlyState = ReadOnly; ReadOnly = false; interfaceUpdateEnabled = false; RichTextBoxContext context = new RichTextBoxContext(); context.SaveContext(this); Rtf = PreviousRtf; UndoUnknownActions(); context.LoadContext(this); interfaceUpdateEnabled = true; ReadOnly = lastReadOnlyState; } private void HandleMouseLeave(object sender, EventArgs e) { HideLinks(); } private void HandleLeave(object sender, EventArgs e) { HideLinks(); } private void HandleKeyDown(object sender, KeyEventArgs e) { if ((e.KeyCode == Keys.ControlKey) && !linksActive) { linksActive = true; ShowRichTextBoxLinks(this); } } private void HandleKeyUp(object sender, KeyEventArgs e) { if ((e.KeyCode == Keys.ControlKey) && linksActive) { HideRichTextBoxLinks(); linksActive = false; } } private void HandleMouseClick(object sender, MouseEventArgs e) { if ((e.Button == MouseButtons.Left) && linksActive) { int linkStart; int linkEnd; int mousePointerCharIndex = GetCharIndexFromPosition(e.Location); SelectionStart = mousePointerCharIndex; SelectionLength = 1; do { if (SelectionFont.Underline && (SelectionStart > 0) && (SelectedText[0] != '\n')) { SelectionStart -= 1; } else { linkStart = SelectionStart; SelectionLength = 1; if (SelectedText[0] == '\n') linkStart += 1; else if (!SelectionFont.Underline || (SelectionStart > 0)) linkStart += 1; break; } } while (true); SelectionStart = mousePointerCharIndex; SelectionLength = 1; do { if (SelectionFont.Underline && (SelectionStart < Text.Length) && (SelectedText[0] != '\n')) { SelectionStart += 1; } else { linkEnd = SelectionStart; if (!SelectionFont.Underline || (SelectionStart < Text.Length)) linkEnd -= 1; if ((linkEnd + 1) <= Text.Length) linkEnd += 1; break; } } while (true); if (linkStart < linkEnd) { string link = Text.Substring(linkStart, linkEnd - linkStart); if (link != "") { foreach (var linkMatch in LinkMatches) { if ((link.Length >= linkMatch.StartString.Length) && ( (linkMatch.CaseSensitive && (link.Substring(0, linkMatch.StartString.Length) == linkMatch.StartString)) || (!linkMatch.CaseSensitive && (link.ToLowerInvariant().Substring(0, linkMatch.StartString.Length) == linkMatch.StartString.ToLower())))) { string linkNumber = link.Substring(linkMatch.StartString.Length).Trim(new char[] { ' ', '#' }); if (int.TryParse(linkNumber, out int number)) { linkMatch.ExecuteLinkAction(number); break; } } } LinkClick?.Invoke(link); HideRichTextBoxLinks(); linksActive = false; } } } } public void ShowInWindow() { Form textForm = new Form { Name = "FormRichTextBox", Width = 600, Height = 300, FormBorderStyle = FormBorderStyle.Sizable, Text = Title, Font = new Font(Font.FontFamily, Font.Size), StartPosition = FormStartPosition.CenterScreen, Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath), }; RichTextBoxEx richTextBox = new RichTextBoxEx { ParentForm = textForm, Dock = DockStyle.Fill, Text = Text, Title = Title, LinkMatches = LinkMatches, ReadOnly = ReadOnly, LinkClick = delegate(string linkText) { LinkClick?.Invoke(linkText); } }; textForm.Controls.Add(richTextBox); textForm.Load += delegate { Theme.UseTheme(textForm); DpiScaling.Apply(textForm); new FormDimensions().Load(textForm, ParentForm); richTextBox.ClearUndo(); }; textForm.FormClosing += delegate { new FormDimensions().Save(textForm, ParentForm); }; textForm.Show(); } private void UpdateInterface() { tsmiUndo.Enabled = CanUndo && !ReadOnly; tsmiRedo.Enabled = CanRedo && !ReadOnly; tsmiCut.Enabled = (SelectionLength != 0) && !ReadOnly; tsmiCopy.Enabled = SelectionLength != 0; tsmiPaste.Enabled = Clipboard.ContainsText() && !ReadOnly; tsmiDelete.Enabled = (SelectionLength != 0) && !ReadOnly; tsmiSelectAll.Enabled = (TextLength > 0) && (SelectionLength < TextLength); } private void HideLinks() { if (linksActive) { HideRichTextBoxLinks(); linksActive = false; } } public void AddContextMenu() { if (ContextMenuStrip == null) { ContextMenuStrip cms = new ContextMenuStrip { ShowImageMargin = false }; tsmiUndo = new ToolStripMenuItem("Undo"); tsmiUndo.Click += (sender, e) => { HideLinks(); RichTextBoxContext context = new RichTextBoxContext(); context.SaveContext(this); UndoUnknownActions(); context.LoadContext(this); if (CanUndo) Undo(); }; tsmiUndo.ShortcutKeys = Keys.Z | Keys.Control; cms.Items.Add(tsmiUndo); tsmiRedo = new ToolStripMenuItem("Redo"); tsmiRedo.Click += (sender, e) => { HideLinks(); if (CanRedo) Redo(); }; tsmiRedo.ShortcutKeys = Keys.Y | Keys.Control; cms.Items.Add(tsmiRedo); cms.Items.Add(new ToolStripSeparator()); tsmiCut = new ToolStripMenuItem("Cut"); tsmiCut.Click += (sender, e) => { HideLinks(); Clipboard.SetText(SelectedText); SelectedText = ""; }; tsmiCut.ShortcutKeys = Keys.X | Keys.Control; cms.Items.Add(tsmiCut); tsmiCopy = new ToolStripMenuItem("Copy"); tsmiCopy.Click += (sender, e) => { HideLinks(); Clipboard.SetText(SelectedText); }; tsmiCopy.ShortcutKeys = Keys.C | Keys.Control; cms.Items.Add(tsmiCopy); tsmiPaste = new ToolStripMenuItem("Paste"); tsmiPaste.Click += (sender, e) => { HideLinks(); Paste(); }; tsmiPaste.ShortcutKeys = Keys.V | Keys.Control; cms.Items.Add(tsmiPaste); tsmiDelete = new ToolStripMenuItem("Delete"); tsmiDelete.Click += (sender, e) => { HideLinks(); SelectedText = ""; }; cms.Items.Add(tsmiDelete); cms.Items.Add(new ToolStripSeparator()); tsmiSelectAll = new ToolStripMenuItem("Select All"); tsmiSelectAll.Click += (sender, e) => { HideLinks(); SelectionStart = 0; SelectionLength = Text.Length; }; tsmiSelectAll.ShortcutKeys = Keys.A | Keys.Control; cms.Items.Add(tsmiSelectAll); cms.Items.Add(new ToolStripSeparator()); tsmiFind = new ToolStripMenuItem("Find"); tsmiFind.Click += (sender, e) => { HideLinks(); if (FormFind == null) { FormFind = new FormFind(); FormFind.richTextBox = this; FormFind.Owner = ParentForm; } FormFind.Show(); FormFind.BringToFront(); }; tsmiFind.ShortcutKeys = Keys.F | Keys.Control; cms.Items.Add(tsmiFind); tsmiShowInWindow = new ToolStripMenuItem("Show in window"); tsmiShowInWindow.Click += (sender, e) => { HideLinks(); ShowInWindow(); }; tsmiShowInWindow.ShortcutKeys = Keys.W | Keys.Control; cms.Items.Add(tsmiShowInWindow); cms.Opening += delegate { HideLinks(); UpdateInterface(); }; ContextMenuStrip = cms; } } } public class RichTextBoxContext { private Point oldScrollPoint; private Point oldScrollOffset; private int oldStart; private int oldLength; [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp); [DllImport("user32.dll")] private static extern int SendMessage(IntPtr hwndLock, Int32 wMsg, Int32 wParam, ref Point pt); [DllImport("User32.dll")] static extern int GetScrollPos(IntPtr hWnd, int nBar); [DllImport("user32.dll")] static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw); private const int WM_SETREDRAW = 0x0b; const int WM_USER = 0x400; const int EM_HIDESELECTION = WM_USER + 63; const int EM_GETEVENTMASK = WM_USER + 59; const int EM_SETEVENTMASK = WM_USER + 69; const int EM_GETSCROLLPOS = WM_USER + 221; const int EM_SETSCROLLPOS = WM_USER + 222; public void SaveContext(RichTextBox richTextBox) { SendMessage(richTextBox.Handle, WM_SETREDRAW, (IntPtr)0, IntPtr.Zero); oldScrollPoint = new Point(); SendMessage(richTextBox.Handle, EM_GETSCROLLPOS, 0, ref oldScrollPoint); oldScrollOffset = richTextBox.AutoScrollOffset; oldStart = richTextBox.SelectionStart; oldLength = richTextBox.SelectionLength; } public void LoadContext(RichTextBox richTextBox) { richTextBox.SelectionStart = oldStart; richTextBox.SelectionLength = oldLength; richTextBox.AutoScrollOffset = oldScrollOffset; SendMessage(richTextBox.Handle, EM_SETSCROLLPOS, 0, ref oldScrollPoint); SendMessage(richTextBox.Handle, WM_SETREDRAW, (IntPtr)1, IntPtr.Zero); richTextBox.Invalidate(); } } public class LinkMatch { public string StartString; public bool CaseSensitive; public delegate bool MatchHandler(string inContent, int startIndex, out string outContent); public delegate void LinkActionHandler(int number); public event MatchHandler Match; public event LinkActionHandler LinkAction; public LinkMatch(string startString, MatchHandler matchHandler, LinkActionHandler action) { StartString = startString; Match = matchHandler; LinkAction = action; } public bool ExecuteMatch(string inContent, int startIndex, out string outContent) { return Match(inContent, startIndex, out outContent); } public void ExecuteLinkAction(int number) { LinkAction?.Invoke(number); } /// /// Check text content for occurence number after start text of numeric link (e.g. ABC 12345 or ABC12345). /// /// Text after startString which should be checked /// Found second part of link after startString /// public static bool MatchNumber(string content, int startIndex, out string linkText) { linkText = ""; int linkLength = 0; int numberStart = -1; // Try to connect to following number numberStart = startIndex; while (((content.Length - numberStart) >= 1) && ((content[numberStart] == ' ') || (content[numberStart] == '#'))) { numberStart = numberStart + 1; linkLength += 1; } int i = 0; while ((i < (content.Length - numberStart)) && (content[numberStart + i] >= '0') && (content[numberStart + i] <= '9')) { i++; } var number = content.Substring(numberStart, i); if (int.TryParse(number, out int intNumber)) { linkLength += number.Length; linkText = content.Substring(startIndex, linkLength); } return linkText != ""; } } }