source: Common/RichTextBoxEx.cs

Last change on this file was 14, checked in by chronos, 21 months ago
  • Modified: Various improvements.
File size: 21.8 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.Drawing;
4using System.Runtime.InteropServices;
5using System.Windows.Forms;
6
7namespace Common
8{
9 public partial class RichTextBoxEx : RichTextBox
10 {
11 private bool linksActive;
12 public delegate void LinkClickHandler(string link);
13 public event LinkClickHandler LinkClick;
14 public string Title { get; set; }
15 public Form ParentForm;
16 public List<LinkMatch> LinkMatches;
17 public FormFind FormFind;
18 public string PreviousRtf;
19 private ToolStripMenuItem tsmiUndo;
20 private ToolStripMenuItem tsmiRedo;
21 private ToolStripMenuItem tsmiCut;
22 private ToolStripMenuItem tsmiCopy;
23 private ToolStripMenuItem tsmiPaste;
24 private ToolStripMenuItem tsmiDelete;
25 private ToolStripMenuItem tsmiSelectAll;
26 private ToolStripMenuItem tsmiFind;
27 private ToolStripMenuItem tsmiShowInWindow;
28 private bool interfaceUpdateEnabled = true;
29
30 public RichTextBoxEx()
31 {
32 //InitializeComponent();
33 KeyDown += HandleKeyDown;
34 KeyUp += HandleKeyUp;
35 MouseClick += HandleMouseClick;
36 MouseLeave += HandleMouseLeave;
37 Leave += HandleLeave;
38 SelectionChanged += delegate
39 {
40 if (interfaceUpdateEnabled) UpdateInterface();
41 };
42 AddContextMenu();
43 LinkMatches = new List<LinkMatch>();
44 }
45
46 protected override void OnPaint(PaintEventArgs pe)
47 {
48 base.OnPaint(pe);
49 }
50
51 private void ShowRichTextBoxLinks(RichTextBox richTextBox)
52 {
53 bool lastReadOnlyState = ReadOnly;
54 ReadOnly = false;
55 interfaceUpdateEnabled = false;
56 PreviousRtf = Rtf;
57 RichTextBoxContext context = new RichTextBoxContext();
58 context.SaveContext(this);
59 List<int> linkMatchStart = new List<int>();
60 foreach (var linkMatch in LinkMatches)
61 {
62 linkMatchStart.Add(-2);
63 }
64
65 List<string> linkMatchStartString = new List<string>();
66 foreach (var linkMatch in LinkMatches)
67 linkMatchStartString.Add(linkMatch.StartString.ToLowerInvariant());
68
69 string content = richTextBox.Text;
70 string contentLowerCase = content.ToLowerInvariant();
71
72 RichTextBox tempRichTextBox = new RichTextBox();
73 tempRichTextBox.Rtf = richTextBox.Rtf;
74
75 int contentStart = 0;
76 do
77 {
78 int firstIndex = content.Length;
79 LinkMatch firstMatch = null;
80 if ((content.Length - contentStart) > 0)
81 {
82 int i = 0;
83 foreach (var linkMatch in LinkMatches)
84 {
85 if ((linkMatchStart[i] < contentStart) && (linkMatchStart[i] != -1))
86 {
87 if (linkMatch.CaseSensitive)
88 {
89 linkMatchStart[i] = content.IndexOf(linkMatch.StartString, contentStart, StringComparison.Ordinal);
90 } else
91 {
92 linkMatchStart[i] = contentLowerCase.IndexOf(linkMatchStartString[i], contentStart, StringComparison.Ordinal);
93 }
94 }
95 if ((linkMatchStart[i] != -1) && (linkMatchStart[i] < firstIndex))
96 {
97 firstMatch = linkMatch;
98 firstIndex = linkMatchStart[i];
99 }
100 i++;
101 }
102 if (firstMatch == null) break;
103 }
104 else break;
105
106 int index = firstIndex;
107 string startString = firstMatch.StartString;
108
109 int linkLength = startString.Length;
110 var selectionStart = index;
111
112 if (firstMatch.ExecuteMatch(content, index + startString.Length, out var linkTextAfter))
113 {
114 if ((firstMatch.StartString != "") || ((firstMatch.StartString == "") && (linkTextAfter.Length == 5) &&
115 ((index < 1) ||
116 ((index >= 1) && ((content[index - 1] == ' ') || (content[index - 1] == '\n'))))
117 &&
118 ((index + linkTextAfter.Length + 1 >= content.Length) ||
119 ((index + linkTextAfter.Length + 1 < content.Length) && ((content[index + linkTextAfter.Length] == ' ') ||
120 (content[index + linkTextAfter.Length] == '\r') || (content[index + linkTextAfter.Length] == '\n') ||
121 (content[index + linkTextAfter.Length] == ',') || (content[index + linkTextAfter.Length] == ';') ||
122 (content[index + linkTextAfter.Length] == '.'))))
123 ))
124 {
125 linkLength += linkTextAfter.Length;
126
127 string linkText = content.Substring(index, linkLength);
128 tempRichTextBox.SelectionStart = selectionStart;
129 tempRichTextBox.SelectionLength = linkText.Length;
130 tempRichTextBox.SelectionFont = new Font(tempRichTextBox.SelectionFont.FontFamily, tempRichTextBox.SelectionFont.Size, FontStyle.Underline);
131 tempRichTextBox.SelectionColor = Color.LightBlue;
132 }
133 }
134 selectionStart += linkLength;
135 if (linkLength == 0) linkLength = 1;
136 contentStart = index + linkLength;
137 } while (true);
138
139 //richTextBox.SelectionStart = 0;
140 richTextBox.SelectAll();
141 richTextBox.SelectedRtf = tempRichTextBox.Rtf;
142
143 context.LoadContext(this);
144 interfaceUpdateEnabled = true;
145 ReadOnly = lastReadOnlyState;
146 }
147
148 [DllImport("user32.dll")]
149 private static extern int SendMessage(IntPtr hwndLock, Int32 wMsg, Int32 wParam, Int32 lParam);
150
151 const int WM_USER = 0x400;
152 const int EM_GETUNDONAME = WM_USER + 86;
153
154 public enum UndoNameId
155 {
156 Unknown = 0,
157 Typing = 1,
158 Delete = 2,
159 DragDrop = 3,
160 Cut = 4,
161 Paste = 5,
162 AutoTable = 6
163 };
164
165 public UndoNameId UndoActionId
166 {
167 get
168 {
169 if (!CanUndo) return UndoNameId.Unknown;
170 UndoNameId n;
171 n = (UndoNameId)SendMessage(Handle, EM_GETUNDONAME, 0, 0);
172 return n;
173 }
174 }
175
176 public void UndoUnknownActions()
177 {
178 int i = 0;
179 while (CanUndo && (UndoActionId == UndoNameId.Unknown))
180 {
181 Undo();
182 i++;
183 if (i > 1000) break;
184 }
185 }
186
187 private void HideRichTextBoxLinks()
188 {
189 bool lastReadOnlyState = ReadOnly;
190 ReadOnly = false;
191 interfaceUpdateEnabled = false;
192 RichTextBoxContext context = new RichTextBoxContext();
193 context.SaveContext(this);
194 Rtf = PreviousRtf;
195 UndoUnknownActions();
196
197 context.LoadContext(this);
198 interfaceUpdateEnabled = true;
199 ReadOnly = lastReadOnlyState;
200 }
201
202 private void HandleMouseLeave(object sender, EventArgs e)
203 {
204 HideLinks();
205 }
206
207 private void HandleLeave(object sender, EventArgs e)
208 {
209 HideLinks();
210 }
211
212 private void HandleKeyDown(object sender, KeyEventArgs e)
213 {
214 if ((e.KeyCode == Keys.ControlKey) && !linksActive)
215 {
216 linksActive = true;
217 ShowRichTextBoxLinks(this);
218 }
219 }
220
221 private void HandleKeyUp(object sender, KeyEventArgs e)
222 {
223 if ((e.KeyCode == Keys.ControlKey) && linksActive)
224 {
225 HideRichTextBoxLinks();
226 linksActive = false;
227 }
228 }
229
230 private void HandleMouseClick(object sender, MouseEventArgs e)
231 {
232 if ((e.Button == MouseButtons.Left) && linksActive)
233 {
234 int linkStart;
235 int linkEnd;
236 int mousePointerCharIndex = GetCharIndexFromPosition(e.Location);
237 SelectionStart = mousePointerCharIndex;
238 SelectionLength = 1;
239 do
240 {
241 if (SelectionFont.Underline && (SelectionStart > 0) && (SelectedText[0] != '\n'))
242 {
243 SelectionStart -= 1;
244 }
245 else
246 {
247 linkStart = SelectionStart;
248 SelectionLength = 1;
249 if (SelectedText[0] == '\n') linkStart += 1;
250 else if (!SelectionFont.Underline || (SelectionStart > 0)) linkStart += 1;
251 break;
252 }
253 } while (true);
254
255 SelectionStart = mousePointerCharIndex;
256 SelectionLength = 1;
257 do
258 {
259 if (SelectionFont.Underline && (SelectionStart < Text.Length) && (SelectedText[0] != '\n'))
260 {
261 SelectionStart += 1;
262 }
263 else
264 {
265 linkEnd = SelectionStart;
266 if (!SelectionFont.Underline || (SelectionStart < Text.Length)) linkEnd -= 1;
267 if ((linkEnd + 1) <= Text.Length) linkEnd += 1;
268 break;
269 }
270 } while (true);
271
272 if (linkStart < linkEnd)
273 {
274 string link = Text.Substring(linkStart, linkEnd - linkStart);
275 if (link != "")
276 {
277 foreach (var linkMatch in LinkMatches)
278 {
279 if ((link.Length >= linkMatch.StartString.Length) && (
280 (linkMatch.CaseSensitive && (link.Substring(0, linkMatch.StartString.Length) == linkMatch.StartString)) ||
281 (!linkMatch.CaseSensitive && (link.ToLowerInvariant().Substring(0, linkMatch.StartString.Length) == linkMatch.StartString.ToLower()))))
282 {
283 string linkNumber = link.Substring(linkMatch.StartString.Length).Trim(new char[] { ' ', '#' });
284 if (int.TryParse(linkNumber, out int number))
285 {
286 linkMatch.ExecuteLinkAction(number);
287 break;
288 }
289 }
290 }
291
292 LinkClick?.Invoke(link);
293 HideRichTextBoxLinks();
294 linksActive = false;
295 }
296 }
297 }
298 }
299
300 public void ShowInWindow()
301 {
302 Form textForm = new Form
303 {
304 Name = "FormRichTextBox",
305 Width = 600,
306 Height = 300,
307 FormBorderStyle = FormBorderStyle.Sizable,
308 Text = Title,
309 Font = new Font(Font.FontFamily, Font.Size),
310 StartPosition = FormStartPosition.CenterScreen,
311 Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath),
312 };
313 RichTextBoxEx richTextBox = new RichTextBoxEx
314 {
315 ParentForm = textForm,
316 Dock = DockStyle.Fill,
317 Text = Text,
318 Title = Title,
319 LinkMatches = LinkMatches,
320 ReadOnly = ReadOnly,
321 LinkClick = delegate(string linkText) { LinkClick?.Invoke(linkText); }
322 };
323 textForm.Controls.Add(richTextBox);
324 textForm.Load += delegate
325 {
326 Theme.UseTheme(textForm);
327 DpiScaling.Apply(textForm);
328 new FormDimensions().Load(textForm, ParentForm);
329 richTextBox.ClearUndo();
330 };
331 textForm.FormClosing += delegate
332 {
333 new FormDimensions().Save(textForm, ParentForm);
334 };
335 textForm.Show();
336 }
337
338 private void UpdateInterface()
339 {
340 tsmiUndo.Enabled = CanUndo && !ReadOnly;
341 tsmiRedo.Enabled = CanRedo && !ReadOnly;
342 tsmiCut.Enabled = (SelectionLength != 0) && !ReadOnly;
343 tsmiCopy.Enabled = SelectionLength != 0;
344 tsmiPaste.Enabled = Clipboard.ContainsText() && !ReadOnly;
345 tsmiDelete.Enabled = (SelectionLength != 0) && !ReadOnly;
346 tsmiSelectAll.Enabled = (TextLength > 0) && (SelectionLength < TextLength);
347 }
348
349 private void HideLinks()
350 {
351 if (linksActive)
352 {
353 HideRichTextBoxLinks();
354 linksActive = false;
355 }
356 }
357
358 public void AddContextMenu()
359 {
360 if (ContextMenuStrip == null)
361 {
362 ContextMenuStrip cms = new ContextMenuStrip { ShowImageMargin = false };
363
364 tsmiUndo = new ToolStripMenuItem("Undo");
365 tsmiUndo.Click += (sender, e) =>
366 {
367 HideLinks();
368 RichTextBoxContext context = new RichTextBoxContext();
369 context.SaveContext(this);
370 UndoUnknownActions();
371 context.LoadContext(this);
372 if (CanUndo) Undo();
373 };
374 tsmiUndo.ShortcutKeys = Keys.Z | Keys.Control;
375 cms.Items.Add(tsmiUndo);
376
377 tsmiRedo = new ToolStripMenuItem("Redo");
378 tsmiRedo.Click += (sender, e) =>
379 {
380 HideLinks();
381 if (CanRedo) Redo();
382 };
383 tsmiRedo.ShortcutKeys = Keys.Y | Keys.Control;
384 cms.Items.Add(tsmiRedo);
385
386 cms.Items.Add(new ToolStripSeparator());
387
388 tsmiCut = new ToolStripMenuItem("Cut");
389 tsmiCut.Click += (sender, e) =>
390 {
391 HideLinks();
392 Clipboard.SetText(SelectedText);
393 SelectedText = "";
394 };
395 tsmiCut.ShortcutKeys = Keys.X | Keys.Control;
396 cms.Items.Add(tsmiCut);
397
398 tsmiCopy = new ToolStripMenuItem("Copy");
399 tsmiCopy.Click += (sender, e) =>
400 {
401 HideLinks();
402 Clipboard.SetText(SelectedText);
403 };
404 tsmiCopy.ShortcutKeys = Keys.C | Keys.Control;
405 cms.Items.Add(tsmiCopy);
406
407 tsmiPaste = new ToolStripMenuItem("Paste");
408 tsmiPaste.Click += (sender, e) =>
409 {
410 HideLinks();
411 Paste();
412 };
413 tsmiPaste.ShortcutKeys = Keys.V | Keys.Control;
414 cms.Items.Add(tsmiPaste);
415
416 tsmiDelete = new ToolStripMenuItem("Delete");
417 tsmiDelete.Click += (sender, e) => {
418 HideLinks();
419 SelectedText = "";
420 };
421 cms.Items.Add(tsmiDelete);
422
423 cms.Items.Add(new ToolStripSeparator());
424
425 tsmiSelectAll = new ToolStripMenuItem("Select All");
426 tsmiSelectAll.Click += (sender, e) => {
427 HideLinks();
428 SelectionStart = 0;
429 SelectionLength = Text.Length;
430 };
431 tsmiSelectAll.ShortcutKeys = Keys.A | Keys.Control;
432 cms.Items.Add(tsmiSelectAll);
433
434 cms.Items.Add(new ToolStripSeparator());
435
436 tsmiFind = new ToolStripMenuItem("Find");
437 tsmiFind.Click += (sender, e) => {
438 HideLinks();
439 if (FormFind == null)
440 {
441 FormFind = new FormFind();
442 FormFind.richTextBox = this;
443 FormFind.Owner = ParentForm;
444 }
445 FormFind.Show();
446 FormFind.BringToFront();
447 };
448 tsmiFind.ShortcutKeys = Keys.F | Keys.Control;
449 cms.Items.Add(tsmiFind);
450
451 tsmiShowInWindow = new ToolStripMenuItem("Show in window");
452 tsmiShowInWindow.Click += (sender, e) =>
453 {
454 HideLinks();
455 ShowInWindow();
456 };
457 tsmiShowInWindow.ShortcutKeys = Keys.W | Keys.Control;
458 cms.Items.Add(tsmiShowInWindow);
459
460 cms.Opening += delegate
461 {
462 HideLinks();
463 UpdateInterface();
464 };
465
466 ContextMenuStrip = cms;
467 }
468 }
469 }
470
471 public class RichTextBoxContext
472 {
473 private Point oldScrollPoint;
474 private Point oldScrollOffset;
475 private int oldStart;
476 private int oldLength;
477
478 [DllImport("user32.dll")]
479 private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
480 [DllImport("user32.dll")]
481 private static extern int SendMessage(IntPtr hwndLock, Int32 wMsg, Int32 wParam, ref Point pt);
482 [DllImport("User32.dll")]
483 static extern int GetScrollPos(IntPtr hWnd, int nBar);
484 [DllImport("user32.dll")]
485 static extern int SetScrollPos(IntPtr hWnd, int nBar, int nPos, bool bRedraw);
486
487 private const int WM_SETREDRAW = 0x0b;
488 const int WM_USER = 0x400;
489 const int EM_HIDESELECTION = WM_USER + 63;
490 const int EM_GETEVENTMASK = WM_USER + 59;
491 const int EM_SETEVENTMASK = WM_USER + 69;
492 const int EM_GETSCROLLPOS = WM_USER + 221;
493 const int EM_SETSCROLLPOS = WM_USER + 222;
494
495 public void SaveContext(RichTextBox richTextBox)
496 {
497 SendMessage(richTextBox.Handle, WM_SETREDRAW, (IntPtr)0, IntPtr.Zero);
498 oldScrollPoint = new Point();
499 SendMessage(richTextBox.Handle, EM_GETSCROLLPOS, 0, ref oldScrollPoint);
500 oldScrollOffset = richTextBox.AutoScrollOffset;
501 oldStart = richTextBox.SelectionStart;
502 oldLength = richTextBox.SelectionLength;
503 }
504
505 public void LoadContext(RichTextBox richTextBox)
506 {
507 richTextBox.SelectionStart = oldStart;
508 richTextBox.SelectionLength = oldLength;
509 richTextBox.AutoScrollOffset = oldScrollOffset;
510 SendMessage(richTextBox.Handle, EM_SETSCROLLPOS, 0, ref oldScrollPoint);
511 SendMessage(richTextBox.Handle, WM_SETREDRAW, (IntPtr)1, IntPtr.Zero);
512 richTextBox.Invalidate();
513 }
514 }
515
516 public class LinkMatch
517 {
518 public string StartString;
519 public bool CaseSensitive;
520 public delegate bool MatchHandler(string inContent, int startIndex, out string outContent);
521 public delegate void LinkActionHandler(int number);
522 public event MatchHandler Match;
523 public event LinkActionHandler LinkAction;
524
525 public LinkMatch(string startString, MatchHandler matchHandler, LinkActionHandler action)
526 {
527 StartString = startString;
528 Match = matchHandler;
529 LinkAction = action;
530 }
531
532 public bool ExecuteMatch(string inContent, int startIndex, out string outContent)
533 {
534 return Match(inContent, startIndex, out outContent);
535 }
536
537 public void ExecuteLinkAction(int number)
538 {
539 LinkAction?.Invoke(number);
540 }
541
542 /// <summary>
543 /// Check text content for occurence number after start text of numeric link (e.g. ABC 12345 or ABC12345).
544 /// </summary>
545 /// <param name="content">Text after startString which should be checked</param>
546 /// <param name="linkText">Found second part of link after startString</param>
547 /// <returns></returns>
548 public static bool MatchNumber(string content, int startIndex, out string linkText)
549 {
550 linkText = "";
551 int linkLength = 0;
552 int numberStart = -1;
553
554 // Try to connect to following number
555 numberStart = startIndex;
556 while (((content.Length - numberStart) >= 1) && ((content[numberStart] == ' ') || (content[numberStart] == '#')))
557 {
558 numberStart = numberStart + 1;
559 linkLength += 1;
560 }
561
562 int i = 0;
563 while ((i < (content.Length - numberStart)) && (content[numberStart + i] >= '0') && (content[numberStart + i] <= '9'))
564 {
565 i++;
566 }
567 var number = content.Substring(numberStart, i);
568
569 if (int.TryParse(number, out int intNumber))
570 {
571 linkLength += number.Length;
572 linkText = content.Substring(startIndex, linkLength);
573 }
574 return linkText != "";
575 }
576 }
577}
Note: See TracBrowser for help on using the repository browser.