* TextBoxBase.cs:
[mono-project.git] / mcs / class / Managed.Windows.Forms / System.Windows.Forms / TextControl.cs
blob05cb7f864e628ca6272a7d8a2172012d28f5809c
1 // Permission is hereby granted, free of charge, to any person obtaining
2 // a copy of this software and associated documentation files (the
3 // "Software"), to deal in the Software without restriction, including
4 // without limitation the rights to use, copy, modify, merge, publish,
5 // distribute, sublicense, and/or sell copies of the Software, and to
6 // permit persons to whom the Software is furnished to do so, subject to
7 // the following conditions:
8 //
9 // The above copyright notice and this permission notice shall be
10 // included in all copies or substantial portions of the Software.
11 //
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 // Copyright (c) 2004-2006 Novell, Inc. (http://www.novell.com)
22 // Authors:
23 // Peter Bartok pbartok@novell.com
27 // NOT COMPLETE
29 // There's still plenty of things missing, I've got most of it planned, just hadn't had
30 // the time to write it all yet.
31 // Stuff missing (in no particular order):
32 // - Align text after RecalculateLine
33 // - Implement tag types for hotlinks, images, etc.
34 // - Implement CaretPgUp/PgDown
36 // NOTE:
37 // selection_start.pos and selection_end.pos are 0-based
38 // selection_start.pos = first selected char
39 // selection_end.pos = first NOT-selected char
41 // FormatText methods are 1-based (as are all tags, LineTag.Start is 1 for
42 // the first character on a line; the reason is that 0 is the position
43 // *before* the first character on a line
46 #undef Debug
48 using System;
49 using System.Collections;
50 using System.Drawing;
51 using System.Drawing.Text;
52 using System.Text;
54 namespace System.Windows.Forms {
55 internal enum LineColor {
56 Red = 0,
57 Black = 1
60 internal enum CaretSelection {
61 Position, // Selection=Caret
62 Word, // Selection=Word under caret
63 Line // Selection=Line under caret
66 internal class FontDefinition {
67 internal String face;
68 internal int size;
69 internal FontStyle add_style;
70 internal FontStyle remove_style;
71 internal Color color;
72 internal Font font_obj;
74 internal FontDefinition() {
75 face = null;
76 size = 0;
77 color = Color.Empty;
81 internal enum CaretDirection {
82 CharForward, // Move a char to the right
83 CharBack, // Move a char to the left
84 LineUp, // Move a line up
85 LineDown, // Move a line down
86 Home, // Move to the beginning of the line
87 End, // Move to the end of the line
88 PgUp, // Move one page up
89 PgDn, // Move one page down
90 CtrlPgUp, // Move caret to the first visible char in the viewport
91 CtrlPgDn, // Move caret to the last visible char in the viewport
92 CtrlHome, // Move to the beginning of the document
93 CtrlEnd, // Move to the end of the document
94 WordBack, // Move to the beginning of the previous word (or beginning of line)
95 WordForward, // Move to the beginning of the next word (or end of line)
96 SelectionStart, // Move to the beginning of the current selection
97 SelectionEnd, // Move to the end of the current selection
98 CharForwardNoWrap, // Move a char forward, but don't wrap onto the next line
99 CharBackNoWrap // Move a char backward, but don't wrap onto the previous line
102 // Being cloneable should allow for nice line and document copies...
103 internal class Line : ICloneable, IComparable {
104 #region Local Variables
105 // Stuff that matters for our line
106 internal StringBuilder text; // Characters for the line
107 internal float[] widths; // Width of each character; always one larger than text.Length
108 internal int space; // Number of elements in text and widths
109 internal int line_no; // Line number
110 internal LineTag tags; // Tags describing the text
111 internal int Y; // Baseline
112 internal int height; // Height of the line (height of tallest tag)
113 internal int ascent; // Ascent of the line (ascent of the tallest tag)
114 internal HorizontalAlignment alignment; // Alignment of the line
115 internal int align_shift; // Pixel shift caused by the alignment
116 internal bool soft_break; // Tag is 'broken soft' and continuation from previous line
117 internal int indent; // Left indent for the first line
118 internal int hanging_indent; // Hanging indent (left indent for all but the first line)
119 internal int right_indent; // Right indent for all lines
120 internal bool carriage_return;
123 // Stuff that's important for the tree
124 internal Line parent; // Our parent line
125 internal Line left; // Line with smaller line number
126 internal Line right; // Line with higher line number
127 internal LineColor color; // We're doing a black/red tree. this is the node color
128 internal int DEFAULT_TEXT_LEN; //
129 internal static StringFormat string_format; // For calculating widths/heights
130 internal bool recalc; // Line changed
131 #endregion // Local Variables
133 #region Constructors
134 internal Line() {
135 color = LineColor.Red;
136 left = null;
137 right = null;
138 parent = null;
139 text = null;
140 recalc = true;
141 soft_break = false;
142 alignment = HorizontalAlignment.Left;
144 if (string_format == null) {
145 string_format = new StringFormat(StringFormat.GenericTypographic);
146 string_format.Trimming = StringTrimming.None;
147 string_format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;
151 internal Line(int LineNo, string Text, Font font, Brush color) : this() {
152 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
154 text = new StringBuilder(Text, space);
155 line_no = LineNo;
157 widths = new float[space + 1];
158 tags = new LineTag(this, 1, text.Length);
159 tags.font = font;
160 tags.color = color;
163 internal Line(int LineNo, string Text, HorizontalAlignment align, Font font, Brush color) : this() {
164 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
166 text = new StringBuilder(Text, space);
167 line_no = LineNo;
168 alignment = align;
170 widths = new float[space + 1];
171 tags = new LineTag(this, 1, text.Length);
172 tags.font = font;
173 tags.color = color;
176 internal Line(int LineNo, string Text, LineTag tag) : this() {
177 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
179 text = new StringBuilder(Text, space);
180 line_no = LineNo;
182 widths = new float[space + 1];
183 tags = tag;
186 #endregion // Constructors
188 #region Internal Properties
189 internal int Indent {
190 get {
191 return indent;
194 set {
195 indent = value;
196 recalc = true;
200 internal int HangingIndent {
201 get {
202 return hanging_indent;
205 set {
206 hanging_indent = value;
207 recalc = true;
211 internal int RightIndent {
212 get {
213 return right_indent;
216 set {
217 right_indent = value;
218 recalc = true;
223 internal int Height {
224 get {
225 return height;
228 set {
229 height = value;
233 internal int LineNo {
234 get {
235 return line_no;
238 set {
239 line_no = value;
243 internal string Text {
244 get {
245 return text.ToString();
248 set {
249 text = new StringBuilder(value, value.Length > DEFAULT_TEXT_LEN ? value.Length : DEFAULT_TEXT_LEN);
253 internal HorizontalAlignment Alignment {
254 get {
255 return alignment;
258 set {
259 if (alignment != value) {
260 alignment = value;
261 recalc = true;
265 #if no
266 internal StringBuilder Text {
267 get {
268 return text;
271 set {
272 text = value;
275 #endif
276 #endregion // Internal Properties
278 #region Internal Methods
279 // Make sure we always have enoughs space in text and widths
280 internal void Grow(int minimum) {
281 int length;
282 float[] new_widths;
284 length = text.Length;
286 if ((length + minimum) > space) {
287 // We need to grow; double the size
289 if ((length + minimum) > (space * 2)) {
290 new_widths = new float[length + minimum * 2 + 1];
291 space = length + minimum * 2;
292 } else {
293 new_widths = new float[space * 2 + 1];
294 space *= 2;
296 widths.CopyTo(new_widths, 0);
298 widths = new_widths;
302 internal void Streamline(int lines) {
303 LineTag current;
304 LineTag next;
306 current = this.tags;
307 next = current.next;
309 // Catch what the loop below wont; eliminate 0 length
310 // tags, but only if there are other tags after us
311 while ((current.length == 0) && (next != null)) {
312 tags = next;
313 tags.previous = null;
314 current = next;
315 next = current.next;
318 if (next == null) {
319 return;
322 while (next != null) {
323 // Take out 0 length tags unless it's the last tag in the document
324 if (next.length == 0) {
325 if ((next.next != null) || (line_no != lines)) {
326 current.next = next.next;
327 if (current.next != null) {
328 current.next.previous = current;
330 next = current.next;
331 continue;
334 if (current.Combine(next)) {
335 next = current.next;
336 continue;
339 current = current.next;
340 next = current.next;
344 /// <summary> Find the tag on a line based on the character position, pos is 0-based</summary>
345 internal LineTag FindTag(int pos) {
346 LineTag tag;
348 if (pos == 0) {
349 return tags;
352 tag = this.tags;
354 if (pos >= text.Length) {
355 pos = text.Length - 1;
358 while (tag != null) {
359 if (((tag.start - 1) <= pos) && (pos < (tag.start + tag.length - 1))) {
360 return LineTag.GetFinalTag (tag);
362 tag = tag.next;
364 return null;
367 /// <summary>
368 /// Recalculate a single line using the same char for every character in the line
369 /// </summary>
371 internal bool RecalculatePasswordLine(Graphics g, Document doc) {
372 LineTag tag;
373 int pos;
374 int len;
375 float w;
376 bool ret;
377 int descent;
379 pos = 0;
380 len = this.text.Length;
381 tag = this.tags;
382 ascent = 0;
383 tag.shift = 0;
384 tag.width = 0;
386 this.recalc = false;
387 widths[0] = indent;
388 tag.X = indent;
390 w = g.MeasureString(doc.password_char, tags.font, 10000, string_format).Width;
392 if (this.height != (int)tag.font.Height) {
393 ret = true;
394 } else {
395 ret = false;
398 this.height = (int)tag.font.Height;
399 tag.height = this.height;
401 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
402 this.ascent = tag.ascent;
404 while (pos < len) {
405 tag.width += w;
406 pos++;
407 widths[pos] = widths[pos-1] + w;
410 return ret;
413 /// <summary>
414 /// Go through all tags on a line and recalculate all size-related values;
415 /// returns true if lineheight changed
416 /// </summary>
417 internal bool RecalculateLine(Graphics g, Document doc) {
418 LineTag tag;
419 int pos;
420 int len;
421 SizeF size;
422 float w;
423 int prev_height;
424 bool retval;
425 bool wrapped;
426 Line line;
427 int wrap_pos;
428 float wrap_width;
430 pos = 0;
431 len = this.text.Length;
432 tag = this.tags;
433 prev_height = this.height; // For drawing optimization calculations
434 this.height = 0; // Reset line height
435 this.ascent = 0; // Reset the ascent for the line
436 tag.shift = 0;
437 tag.width = 0;
439 if (this.soft_break) {
440 widths[0] = hanging_indent;
441 } else {
442 widths[0] = indent;
445 this.recalc = false;
446 retval = false;
447 wrapped = false;
449 wrap_pos = 0;
450 wrap_width = 0;
452 while (pos < len) {
453 size = g.MeasureString(this.text.ToString(pos, 1), tag.font, 10000, string_format);
455 while (tag.length == 0) { // We should always have tags after a tag.length==0 unless len==0
456 tag.width = 0;
457 tag.ascent = 0;
458 if (tag.previous != null) {
459 tag.X = tag.previous.X;
460 } else {
461 tag.X = (int)widths[pos];
463 tag = tag.next;
464 tag.width = 0;
465 tag.shift = 0;
468 w = size.Width;
470 if (Char.IsWhiteSpace(text[pos])) {
471 wrap_pos = pos + 1;
472 wrap_width = tag.width + w;
475 if (doc.wrap) {
476 if ((wrap_pos > 0) && (wrap_pos != len) && (widths[pos] + w) + 5 > (doc.viewport_width - this.right_indent)) {
477 pos = wrap_pos;
478 tag.width = wrap_width;
479 doc.Split(this, tag, pos, this.soft_break);
480 this.soft_break = true;
481 len = this.text.Length;
482 retval = true;
483 wrapped = true;
484 } else if (pos > 1 && (widths[pos] + w) > (doc.viewport_width - this.right_indent)) {
485 // No suitable wrap position was found so break right in the middle of a word
486 tag.width = tag.width + w;
487 doc.Split(this, tag, pos, this.soft_break);
488 this.soft_break = true;
489 len = this.text.Length;
490 retval = true;
491 wrapped = true;
495 // Contract all soft lines that follow back into our line
496 if (!wrapped) {
497 tag.width += w;
499 pos++;
501 widths[pos] = widths[pos-1] + w;
503 if (pos == len) {
504 line = doc.GetLine(this.line_no + 1);
505 if ((line != null) && soft_break) {
506 // Pull the two lines together
507 doc.Combine(this.line_no, this.line_no + 1);
508 len = this.text.Length;
509 retval = true;
514 if (pos == (tag.start-1 + tag.length)) {
515 // We just found the end of our current tag
516 tag.height = (int)tag.font.Height;
518 // Check if we're the tallest on the line (so far)
519 if (tag.height > this.height) {
520 this.height = tag.height; // Yep; make sure the line knows
523 if (tag.ascent == 0) {
524 int descent;
526 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
529 if (tag.ascent > this.ascent) {
530 LineTag t;
532 // We have a tag that has a taller ascent than the line;
534 t = tags;
535 while (t != tag) {
536 t.shift = tag.ascent - t.ascent;
537 t = t.next;
540 // Save on our line
541 this.ascent = tag.ascent;
542 } else {
543 tag.shift = this.ascent - tag.ascent;
546 // Update our horizontal starting pixel position
547 if (tag.previous == null) {
548 tag.X = (int)widths[0];
549 } else {
550 tag.X = tag.previous.X + (int)tag.previous.width;
553 tag = tag.next;
554 if (tag != null) {
555 tag.width = 0;
556 tag.shift = 0;
557 wrap_pos = pos;
558 wrap_width = tag.width;
563 if (this.height == 0) {
564 this.height = tags.font.Height;
565 tag.height = this.height;
568 if (prev_height != this.height) {
569 retval = true;
571 return retval;
573 #endregion // Internal Methods
575 #region Administrative
576 public int CompareTo(object obj) {
577 if (obj == null) {
578 return 1;
581 if (! (obj is Line)) {
582 throw new ArgumentException("Object is not of type Line", "obj");
585 if (line_no < ((Line)obj).line_no) {
586 return -1;
587 } else if (line_no > ((Line)obj).line_no) {
588 return 1;
589 } else {
590 return 0;
594 public object Clone() {
595 Line clone;
597 clone = new Line();
599 clone.text = text;
601 if (left != null) {
602 clone.left = (Line)left.Clone();
605 if (left != null) {
606 clone.left = (Line)left.Clone();
609 return clone;
612 internal object CloneLine() {
613 Line clone;
615 clone = new Line();
617 clone.text = text;
619 return clone;
622 public override bool Equals(object obj) {
623 if (obj == null) {
624 return false;
627 if (!(obj is Line)) {
628 return false;
631 if (obj == this) {
632 return true;
635 if (line_no == ((Line)obj).line_no) {
636 return true;
639 return false;
642 public override int GetHashCode() {
643 return base.GetHashCode ();
646 public override string ToString() {
647 return "Line " + line_no;
650 #endregion // Administrative
653 internal class Document : ICloneable, IEnumerable {
654 #region Structures
655 // FIXME - go through code and check for places where
656 // we do explicit comparisons instead of using the compare overloads
657 internal struct Marker {
658 internal Line line;
659 internal LineTag tag;
660 internal int pos;
661 internal int height;
663 public static bool operator<(Marker lhs, Marker rhs) {
664 if (lhs.line.line_no < rhs.line.line_no) {
665 return true;
668 if (lhs.line.line_no == rhs.line.line_no) {
669 if (lhs.pos < rhs.pos) {
670 return true;
673 return false;
676 public static bool operator>(Marker lhs, Marker rhs) {
677 if (lhs.line.line_no > rhs.line.line_no) {
678 return true;
681 if (lhs.line.line_no == rhs.line.line_no) {
682 if (lhs.pos > rhs.pos) {
683 return true;
686 return false;
689 public static bool operator==(Marker lhs, Marker rhs) {
690 if ((lhs.line.line_no == rhs.line.line_no) && (lhs.pos == rhs.pos)) {
691 return true;
693 return false;
696 public static bool operator!=(Marker lhs, Marker rhs) {
697 if ((lhs.line.line_no != rhs.line.line_no) || (lhs.pos != rhs.pos)) {
698 return true;
700 return false;
703 public void Combine(Line move_to_line, int move_to_line_length) {
704 line = move_to_line;
705 pos += move_to_line_length;
706 tag = LineTag.FindTag(line, pos);
709 // This is for future use, right now Document.Split does it by hand, with some added shortcut logic
710 public void Split(Line move_to_line, int split_at) {
711 line = move_to_line;
712 pos -= split_at;
713 tag = LineTag.FindTag(line, pos);
716 public override bool Equals(object obj) {
717 return this==(Marker)obj;
720 public override int GetHashCode() {
721 return base.GetHashCode ();
724 public override string ToString() {
725 return "Marker Line " + line + ", Position " + pos;
729 #endregion Structures
731 #region Local Variables
732 private Line document;
733 private int lines;
734 private Line sentinel;
735 private int document_id;
736 private Random random = new Random();
737 internal string password_char;
738 private StringBuilder password_cache;
739 private bool calc_pass;
740 private int char_count;
742 private bool no_recalc;
743 private bool recalc_pending;
744 private int recalc_start;
745 private int recalc_end;
746 private bool recalc_optimize;
748 internal bool multiline;
749 internal bool wrap;
751 internal UndoClass undo;
753 internal Marker caret;
754 internal Marker selection_start;
755 internal Marker selection_end;
756 internal bool selection_visible;
757 internal Marker selection_anchor;
758 internal Marker selection_prev;
759 internal bool selection_end_anchor;
761 internal int viewport_x;
762 internal int viewport_y; // The visible area of the document
763 internal int viewport_width;
764 internal int viewport_height;
766 internal int document_x; // Width of the document
767 internal int document_y; // Height of the document
769 internal Rectangle invalid;
771 internal int crlf_size; // 1 or 2, depending on whether we use \r\n or just \n
773 internal TextBoxBase owner; // Who's owning us?
774 static internal int caret_width = 1;
775 static internal int caret_shift = 1;
776 #endregion // Local Variables
778 #region Constructors
779 internal Document(TextBoxBase owner) {
780 lines = 0;
782 this.owner = owner;
784 multiline = true;
785 password_char = "";
786 calc_pass = false;
787 no_recalc = false;
788 recalc_pending = false;
790 // Tree related stuff
791 sentinel = new Line();
792 sentinel.color = LineColor.Black;
794 document = sentinel;
796 // We always have a blank line
797 owner.HandleCreated += new EventHandler(owner_HandleCreated);
798 owner.VisibleChanged += new EventHandler(owner_VisibleChanged);
800 Add(1, "", owner.Font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.ForeColor));
801 Line l = GetLine (1);
802 l.soft_break = true;
804 undo = new UndoClass(this);
806 selection_visible = false;
807 selection_start.line = this.document;
808 selection_start.pos = 0;
809 selection_start.tag = selection_start.line.tags;
810 selection_end.line = this.document;
811 selection_end.pos = 0;
812 selection_end.tag = selection_end.line.tags;
813 selection_anchor.line = this.document;
814 selection_anchor.pos = 0;
815 selection_anchor.tag = selection_anchor.line.tags;
816 caret.line = this.document;
817 caret.pos = 0;
818 caret.tag = caret.line.tags;
820 viewport_x = 0;
821 viewport_y = 0;
823 crlf_size = 2;
825 // Default selection is empty
827 document_id = random.Next();
829 #endregion
831 #region Internal Properties
832 internal Line Root {
833 get {
834 return document;
837 set {
838 document = value;
842 internal int Lines {
843 get {
844 return lines;
848 internal Line CaretLine {
849 get {
850 return caret.line;
854 internal int CaretPosition {
855 get {
856 return caret.pos;
860 internal Point Caret {
861 get {
862 return new Point((int)caret.tag.line.widths[caret.pos] + caret.line.align_shift, caret.line.Y);
866 internal LineTag CaretTag {
867 get {
868 return caret.tag;
871 set {
872 caret.tag = value;
876 internal int CRLFSize {
877 get {
878 return crlf_size;
881 set {
882 crlf_size = value;
886 internal string PasswordChar {
887 get {
888 return password_char;
891 set {
892 password_char = value;
893 if ((password_char.Length != 0) && (password_char[0] != '\0')) {
894 char ch;
896 calc_pass = true;
897 ch = value[0];
898 password_cache = new StringBuilder(1024);
899 for (int i = 0; i < 1024; i++) {
900 password_cache.Append(ch);
902 } else {
903 calc_pass = false;
904 password_cache = null;
909 internal int ViewPortX {
910 get {
911 return viewport_x;
914 set {
915 viewport_x = value;
919 internal int Length {
920 get {
921 return char_count + lines - 1; // Add \n for each line but the last
925 private int CharCount {
926 get {
927 return char_count;
930 set {
931 char_count = value;
933 if (LengthChanged != null) {
934 LengthChanged(this, EventArgs.Empty);
939 ///<summary>Setting NoRecalc to true will prevent the document from being recalculated.
940 ///This ensures that coordinates of added text are predictable after adding the text even with wrapped view</summary>
941 internal bool NoRecalc {
942 get {
943 return no_recalc;
946 set {
947 no_recalc = value;
948 if (!no_recalc && recalc_pending) {
949 RecalculateDocument(owner.CreateGraphicsInternal(), recalc_start, recalc_end, recalc_optimize);
950 recalc_pending = false;
955 internal int ViewPortY {
956 get {
957 return viewport_y;
960 set {
961 viewport_y = value;
965 internal int ViewPortWidth {
966 get {
967 return viewport_width;
970 set {
971 viewport_width = value;
975 internal int ViewPortHeight {
976 get {
977 return viewport_height;
980 set {
981 viewport_height = value;
986 internal int Width {
987 get {
988 return this.document_x;
992 internal int Height {
993 get {
994 return this.document_y;
998 internal bool SelectionVisible {
999 get {
1000 return selection_visible;
1004 internal bool Wrap {
1005 get {
1006 return wrap;
1009 set {
1010 wrap = value;
1014 #endregion // Internal Properties
1016 #region Private Methods
1017 // For debugging
1018 internal int DumpTree(Line line, bool with_tags) {
1019 int total;
1021 total = 1;
1023 Console.Write("Line {0} [# {1}], Y: {2}, soft: {3}, Text {4}",
1024 line.line_no, line.GetHashCode(), line.Y, line.soft_break,
1025 line.text != null ? line.text.ToString() : "undefined");
1027 if (line.left == sentinel) {
1028 Console.Write(", left = sentinel");
1029 } else if (line.left == null) {
1030 Console.Write(", left = NULL");
1033 if (line.right == sentinel) {
1034 Console.Write(", right = sentinel");
1035 } else if (line.right == null) {
1036 Console.Write(", right = NULL");
1039 Console.WriteLine("");
1041 if (with_tags) {
1042 LineTag tag;
1043 int count;
1044 int length;
1046 tag = line.tags;
1047 count = 1;
1048 length = 0;
1049 Console.Write(" Tags: ");
1050 while (tag != null) {
1051 Console.Write("{0} <{1}>-<{2}> ", count++, tag.start, tag.length);
1052 length += tag.length;
1054 if (tag.line != line) {
1055 Console.Write("BAD line link");
1056 throw new Exception("Bad line link in tree");
1058 tag = tag.next;
1059 if (tag != null) {
1060 Console.Write(", ");
1063 if (length > line.text.Length) {
1064 throw new Exception(String.Format("Length of tags more than length of text on line (expected {0} calculated {1})", line.text.Length, length));
1065 } else if (length < line.text.Length) {
1066 throw new Exception(String.Format("Length of tags less than length of text on line (expected {0} calculated {1})", line.text.Length, length));
1068 Console.WriteLine("");
1070 if (line.left != null) {
1071 if (line.left != sentinel) {
1072 total += DumpTree(line.left, with_tags);
1074 } else {
1075 if (line != sentinel) {
1076 throw new Exception("Left should not be NULL");
1080 if (line.right != null) {
1081 if (line.right != sentinel) {
1082 total += DumpTree(line.right, with_tags);
1084 } else {
1085 if (line != sentinel) {
1086 throw new Exception("Right should not be NULL");
1090 for (int i = 1; i <= this.lines; i++) {
1091 if (GetLine(i) == null) {
1092 throw new Exception(String.Format("Hole in line order, missing {0}", i));
1096 if (line == this.Root) {
1097 if (total < this.lines) {
1098 throw new Exception(String.Format("Not enough nodes in tree, found {0}, expected {1}", total, this.lines));
1099 } else if (total > this.lines) {
1100 throw new Exception(String.Format("Too many nodes in tree, found {0}, expected {1}", total, this.lines));
1104 return total;
1107 private void SetSelectionVisible (bool value)
1109 selection_visible = value;
1111 // cursor and selection are enemies, we can't have both in the same room at the same time
1112 if (owner.IsHandleCreated)
1113 XplatUI.CaretVisible (owner.Handle, !selection_visible);
1116 private void DecrementLines(int line_no) {
1117 int current;
1119 current = line_no;
1120 while (current <= lines) {
1121 GetLine(current).line_no--;
1122 current++;
1124 return;
1127 private void IncrementLines(int line_no) {
1128 int current;
1130 current = this.lines;
1131 while (current >= line_no) {
1132 GetLine(current).line_no++;
1133 current--;
1135 return;
1138 private void RebalanceAfterAdd(Line line1) {
1139 Line line2;
1141 while ((line1 != document) && (line1.parent.color == LineColor.Red)) {
1142 if (line1.parent == line1.parent.parent.left) {
1143 line2 = line1.parent.parent.right;
1145 if ((line2 != null) && (line2.color == LineColor.Red)) {
1146 line1.parent.color = LineColor.Black;
1147 line2.color = LineColor.Black;
1148 line1.parent.parent.color = LineColor.Red;
1149 line1 = line1.parent.parent;
1150 } else {
1151 if (line1 == line1.parent.right) {
1152 line1 = line1.parent;
1153 RotateLeft(line1);
1156 line1.parent.color = LineColor.Black;
1157 line1.parent.parent.color = LineColor.Red;
1159 RotateRight(line1.parent.parent);
1161 } else {
1162 line2 = line1.parent.parent.left;
1164 if ((line2 != null) && (line2.color == LineColor.Red)) {
1165 line1.parent.color = LineColor.Black;
1166 line2.color = LineColor.Black;
1167 line1.parent.parent.color = LineColor.Red;
1168 line1 = line1.parent.parent;
1169 } else {
1170 if (line1 == line1.parent.left) {
1171 line1 = line1.parent;
1172 RotateRight(line1);
1175 line1.parent.color = LineColor.Black;
1176 line1.parent.parent.color = LineColor.Red;
1177 RotateLeft(line1.parent.parent);
1181 document.color = LineColor.Black;
1184 private void RebalanceAfterDelete(Line line1) {
1185 Line line2;
1187 while ((line1 != document) && (line1.color == LineColor.Black)) {
1188 if (line1 == line1.parent.left) {
1189 line2 = line1.parent.right;
1190 if (line2.color == LineColor.Red) {
1191 line2.color = LineColor.Black;
1192 line1.parent.color = LineColor.Red;
1193 RotateLeft(line1.parent);
1194 line2 = line1.parent.right;
1196 if ((line2.left.color == LineColor.Black) && (line2.right.color == LineColor.Black)) {
1197 line2.color = LineColor.Red;
1198 line1 = line1.parent;
1199 } else {
1200 if (line2.right.color == LineColor.Black) {
1201 line2.left.color = LineColor.Black;
1202 line2.color = LineColor.Red;
1203 RotateRight(line2);
1204 line2 = line1.parent.right;
1206 line2.color = line1.parent.color;
1207 line1.parent.color = LineColor.Black;
1208 line2.right.color = LineColor.Black;
1209 RotateLeft(line1.parent);
1210 line1 = document;
1212 } else {
1213 line2 = line1.parent.left;
1214 if (line2.color == LineColor.Red) {
1215 line2.color = LineColor.Black;
1216 line1.parent.color = LineColor.Red;
1217 RotateRight(line1.parent);
1218 line2 = line1.parent.left;
1220 if ((line2.right.color == LineColor.Black) && (line2.left.color == LineColor.Black)) {
1221 line2.color = LineColor.Red;
1222 line1 = line1.parent;
1223 } else {
1224 if (line2.left.color == LineColor.Black) {
1225 line2.right.color = LineColor.Black;
1226 line2.color = LineColor.Red;
1227 RotateLeft(line2);
1228 line2 = line1.parent.left;
1230 line2.color = line1.parent.color;
1231 line1.parent.color = LineColor.Black;
1232 line2.left.color = LineColor.Black;
1233 RotateRight(line1.parent);
1234 line1 = document;
1238 line1.color = LineColor.Black;
1241 private void RotateLeft(Line line1) {
1242 Line line2 = line1.right;
1244 line1.right = line2.left;
1246 if (line2.left != sentinel) {
1247 line2.left.parent = line1;
1250 if (line2 != sentinel) {
1251 line2.parent = line1.parent;
1254 if (line1.parent != null) {
1255 if (line1 == line1.parent.left) {
1256 line1.parent.left = line2;
1257 } else {
1258 line1.parent.right = line2;
1260 } else {
1261 document = line2;
1264 line2.left = line1;
1265 if (line1 != sentinel) {
1266 line1.parent = line2;
1270 private void RotateRight(Line line1) {
1271 Line line2 = line1.left;
1273 line1.left = line2.right;
1275 if (line2.right != sentinel) {
1276 line2.right.parent = line1;
1279 if (line2 != sentinel) {
1280 line2.parent = line1.parent;
1283 if (line1.parent != null) {
1284 if (line1 == line1.parent.right) {
1285 line1.parent.right = line2;
1286 } else {
1287 line1.parent.left = line2;
1289 } else {
1290 document = line2;
1293 line2.right = line1;
1294 if (line1 != sentinel) {
1295 line1.parent = line2;
1300 internal void UpdateView(Line line, int pos) {
1301 if (!owner.IsHandleCreated) {
1302 return;
1305 if (no_recalc) {
1306 recalc_start = line.line_no;
1307 recalc_end = line.line_no;
1308 recalc_optimize = true;
1309 recalc_pending = true;
1310 return;
1313 // Optimize invalidation based on Line alignment
1314 if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no, true)) {
1315 // Lineheight changed, invalidate the rest of the document
1316 if ((line.Y - viewport_y) >=0 ) {
1317 // We formatted something that's in view, only draw parts of the screen
1318 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, owner.Height - line.Y - viewport_y));
1319 } else {
1320 // The tag was above the visible area, draw everything
1321 owner.Invalidate();
1323 } else {
1324 switch(line.alignment) {
1325 case HorizontalAlignment.Left: {
1326 owner.Invalidate(new Rectangle((int)line.widths[pos] - viewport_x - 1, line.Y - viewport_y, viewport_width, line.height + 1));
1327 break;
1330 case HorizontalAlignment.Center: {
1331 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, line.height + 1));
1332 break;
1335 case HorizontalAlignment.Right: {
1336 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, (int)line.widths[pos + 1] - viewport_x + line.align_shift, line.height + 1));
1337 break;
1344 // Update display from line, down line_count lines; pos is unused, but required for the signature
1345 internal void UpdateView(Line line, int line_count, int pos) {
1346 if (!owner.IsHandleCreated) {
1347 return;
1350 if (no_recalc) {
1351 recalc_start = line.line_no;
1352 recalc_end = line.line_no + line_count - 1;
1353 recalc_optimize = true;
1354 recalc_pending = true;
1355 return;
1358 if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no + line_count - 1, true)) {
1359 // Lineheight changed, invalidate the rest of the document
1360 if ((line.Y - viewport_y) >=0 ) {
1361 // We formatted something that's in view, only draw parts of the screen
1362 //blah Console.WriteLine("TextControl.cs(981) Invalidate called in UpdateView(line, line_count, pos)");
1363 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, owner.Height - line.Y - viewport_y));
1364 } else {
1365 // The tag was above the visible area, draw everything
1366 //blah Console.WriteLine("TextControl.cs(985) Invalidate called in UpdateView(line, line_count, pos)");
1367 owner.Invalidate();
1369 } else {
1370 Line end_line;
1372 end_line = GetLine(line.line_no + line_count -1);
1373 if (end_line == null) {
1374 end_line = line;
1377 //blah Console.WriteLine("TextControl.cs(996) Invalidate called in UpdateView(line, line_count, pos)");
1378 owner.Invalidate(new Rectangle(0 - viewport_x, line.Y - viewport_y, (int)line.widths[line.text.Length], end_line.Y + end_line.height));
1381 #endregion // Private Methods
1383 #region Internal Methods
1384 // Clear the document and reset state
1385 internal void Empty() {
1387 document = sentinel;
1388 lines = 0;
1390 // We always have a blank line
1391 Add(1, "", owner.Font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.ForeColor));
1392 Line l = GetLine (1);
1393 l.soft_break = true;
1395 this.RecalculateDocument(owner.CreateGraphicsInternal());
1396 PositionCaret(0, 0);
1398 SetSelectionVisible (false);
1400 selection_start.line = this.document;
1401 selection_start.pos = 0;
1402 selection_start.tag = selection_start.line.tags;
1403 selection_end.line = this.document;
1404 selection_end.pos = 0;
1405 selection_end.tag = selection_end.line.tags;
1406 char_count = 0;
1408 viewport_x = 0;
1409 viewport_y = 0;
1411 document_x = 0;
1412 document_y = 0;
1415 internal void PositionCaret(Line line, int pos) {
1416 if (owner.IsHandleCreated) {
1417 undo.RecordCursor();
1420 caret.tag = line.FindTag(pos);
1421 caret.line = line;
1422 caret.pos = pos;
1423 caret.height = caret.tag.height;
1425 if (owner.IsHandleCreated) {
1426 if (owner.Focused) {
1427 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1430 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1436 internal void PositionCaret(int x, int y) {
1437 if (!owner.IsHandleCreated) {
1438 return;
1441 undo.RecordCursor();
1443 caret.tag = FindCursor(x, y, out caret.pos);
1444 caret.line = caret.tag.line;
1445 caret.height = caret.tag.height;
1447 if (owner.Focused) {
1448 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1451 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1454 internal void CaretHasFocus() {
1455 if ((caret.tag != null) && owner.IsHandleCreated) {
1456 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1457 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1459 DisplayCaret ();
1463 internal void CaretLostFocus() {
1464 if (!owner.IsHandleCreated) {
1465 return;
1467 XplatUI.DestroyCaret(owner.Handle);
1470 internal void AlignCaret() {
1471 if (!owner.IsHandleCreated) {
1472 return;
1475 undo.RecordCursor();
1477 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1478 caret.height = caret.tag.height;
1480 if (owner.Focused) {
1481 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1482 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1483 DisplayCaret ();
1486 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1489 internal void UpdateCaret() {
1490 if (!owner.IsHandleCreated || caret.tag == null) {
1491 return;
1494 undo.RecordCursor();
1496 if (caret.tag.height != caret.height) {
1497 caret.height = caret.tag.height;
1498 if (owner.Focused) {
1499 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1503 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1505 DisplayCaret ();
1507 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1510 internal void DisplayCaret() {
1511 if (!owner.IsHandleCreated) {
1512 return;
1515 if (owner.Focused && !selection_visible) {
1516 XplatUI.CaretVisible(owner.Handle, true);
1520 internal void HideCaret() {
1521 if (!owner.IsHandleCreated) {
1522 return;
1525 if (owner.Focused) {
1526 XplatUI.CaretVisible(owner.Handle, false);
1530 internal void MoveCaret(CaretDirection direction) {
1531 // FIXME should we use IsWordSeparator to detect whitespace, instead
1532 // of looking for actual spaces in the Word move cases?
1534 bool nowrap = false;
1535 switch(direction) {
1536 case CaretDirection.CharForwardNoWrap:
1537 nowrap = true;
1538 goto case CaretDirection.CharForward;
1539 case CaretDirection.CharForward: {
1540 caret.pos++;
1541 if (caret.pos > caret.line.text.Length) {
1542 if (multiline && !nowrap) {
1543 // Go into next line
1544 if (caret.line.line_no < this.lines) {
1545 caret.line = GetLine(caret.line.line_no+1);
1546 caret.pos = 0;
1547 caret.tag = caret.line.tags;
1548 } else {
1549 caret.pos--;
1551 } else {
1552 // Single line; we stay where we are
1553 caret.pos--;
1555 } else {
1556 if ((caret.tag.start - 1 + caret.tag.length) < caret.pos) {
1557 caret.tag = caret.tag.next;
1560 UpdateCaret();
1561 return;
1564 case CaretDirection.CharBackNoWrap:
1565 nowrap = true;
1566 goto case CaretDirection.CharBack;
1567 case CaretDirection.CharBack: {
1568 if (caret.pos > 0) {
1569 // caret.pos--; // folded into the if below
1570 if (--caret.pos > 0) {
1571 if (caret.tag.start > caret.pos) {
1572 caret.tag = caret.tag.previous;
1575 } else {
1576 if (caret.line.line_no > 1 && !nowrap) {
1577 caret.line = GetLine(caret.line.line_no - 1);
1578 caret.pos = caret.line.text.Length;
1579 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1582 UpdateCaret();
1583 return;
1586 case CaretDirection.WordForward: {
1587 int len;
1589 len = caret.line.text.Length;
1590 if (caret.pos < len) {
1591 while ((caret.pos < len) && (caret.line.text[caret.pos] != ' ')) {
1592 caret.pos++;
1594 if (caret.pos < len) {
1595 // Skip any whitespace
1596 while ((caret.pos < len) && (caret.line.text[caret.pos] == ' ')) {
1597 caret.pos++;
1600 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1601 } else {
1602 if (caret.line.line_no < this.lines) {
1603 caret.line = GetLine(caret.line.line_no + 1);
1604 caret.pos = 0;
1605 caret.tag = caret.line.tags;
1608 UpdateCaret();
1609 return;
1612 case CaretDirection.WordBack: {
1613 if (caret.pos > 0) {
1614 caret.pos--;
1616 while ((caret.pos > 0) && (caret.line.text[caret.pos] == ' ')) {
1617 caret.pos--;
1620 while ((caret.pos > 0) && (caret.line.text[caret.pos] != ' ')) {
1621 caret.pos--;
1624 if (caret.line.text.ToString(caret.pos, 1) == " ") {
1625 if (caret.pos != 0) {
1626 caret.pos++;
1627 } else {
1628 caret.line = GetLine(caret.line.line_no - 1);
1629 caret.pos = caret.line.text.Length;
1632 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1633 } else {
1634 if (caret.line.line_no > 1) {
1635 caret.line = GetLine(caret.line.line_no - 1);
1636 caret.pos = caret.line.text.Length;
1637 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1640 UpdateCaret();
1641 return;
1644 case CaretDirection.LineUp: {
1645 if (caret.line.line_no > 1) {
1646 int pixel;
1648 pixel = (int)caret.line.widths[caret.pos];
1649 PositionCaret(pixel, GetLine(caret.line.line_no - 1).Y);
1651 DisplayCaret ();
1653 return;
1656 case CaretDirection.LineDown: {
1657 if (caret.line.line_no < lines) {
1658 int pixel;
1660 pixel = (int)caret.line.widths[caret.pos];
1661 PositionCaret(pixel, GetLine(caret.line.line_no + 1).Y);
1663 DisplayCaret ();
1665 return;
1668 case CaretDirection.Home: {
1669 if (caret.pos > 0) {
1670 caret.pos = 0;
1671 caret.tag = caret.line.tags;
1672 UpdateCaret();
1674 return;
1677 case CaretDirection.End: {
1678 if (caret.pos < caret.line.text.Length) {
1679 caret.pos = caret.line.text.Length;
1680 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1681 UpdateCaret();
1683 return;
1686 case CaretDirection.PgUp: {
1688 int new_y, y_offset;
1690 if (viewport_y == 0) {
1692 // This should probably be handled elsewhere
1693 if (!(owner is RichTextBox)) {
1694 // Page down doesn't do anything in a regular TextBox
1695 // if the bottom of the document
1696 // is already visible, the page and the caret stay still
1697 return;
1700 // We're just placing the caret at the end of the document, no scrolling needed
1701 owner.vscroll.Value = 0;
1702 Line line = GetLine (1);
1703 PositionCaret (line, 0);
1706 y_offset = caret.line.Y - viewport_y;
1707 new_y = caret.line.Y - viewport_height;
1709 owner.vscroll.Value = Math.Max (new_y, 0);
1710 PositionCaret ((int)caret.line.widths[caret.pos], y_offset + viewport_y);
1711 return;
1714 case CaretDirection.PgDn: {
1715 int new_y, y_offset;
1717 if ((viewport_y + viewport_height) > document_y) {
1719 // This should probably be handled elsewhere
1720 if (!(owner is RichTextBox)) {
1721 // Page up doesn't do anything in a regular TextBox
1722 // if the bottom of the document
1723 // is already visible, the page and the caret stay still
1724 return;
1727 // We're just placing the caret at the end of the document, no scrolling needed
1728 owner.vscroll.Value = owner.vscroll.Maximum - viewport_height + 1;
1729 Line line = GetLine (lines);
1730 PositionCaret (line, line.Text.Length);
1733 y_offset = caret.line.Y - viewport_y;
1734 new_y = caret.line.Y + viewport_height;
1736 owner.vscroll.Value = Math.Min (new_y, owner.vscroll.Maximum - viewport_height + 1);
1737 PositionCaret ((int)caret.line.widths[caret.pos], y_offset + viewport_y);
1739 return;
1742 case CaretDirection.CtrlPgUp: {
1743 PositionCaret(0, viewport_y);
1744 DisplayCaret ();
1745 return;
1748 case CaretDirection.CtrlPgDn: {
1749 Line line;
1750 LineTag tag;
1751 int index;
1753 tag = FindTag(0, viewport_y + viewport_height, out index, false);
1754 if (tag.line.line_no > 1) {
1755 line = GetLine(tag.line.line_no - 1);
1756 } else {
1757 line = tag.line;
1759 PositionCaret(line, line.Text.Length);
1760 DisplayCaret ();
1761 return;
1764 case CaretDirection.CtrlHome: {
1765 caret.line = GetLine(1);
1766 caret.pos = 0;
1767 caret.tag = caret.line.tags;
1769 UpdateCaret();
1770 return;
1773 case CaretDirection.CtrlEnd: {
1774 caret.line = GetLine(lines);
1775 caret.pos = caret.line.text.Length;
1776 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1778 UpdateCaret();
1779 return;
1782 case CaretDirection.SelectionStart: {
1783 caret.line = selection_start.line;
1784 caret.pos = selection_start.pos;
1785 caret.tag = selection_start.tag;
1787 UpdateCaret();
1788 return;
1791 case CaretDirection.SelectionEnd: {
1792 caret.line = selection_end.line;
1793 caret.pos = selection_end.pos;
1794 caret.tag = selection_end.tag;
1796 UpdateCaret();
1797 return;
1802 // Draw the document
1803 internal void Draw(Graphics g, Rectangle clip) {
1804 Line line; // Current line being drawn
1805 LineTag tag; // Current tag being drawn
1806 int start; // First line to draw
1807 int end; // Last line to draw
1808 StringBuilder text; // String representing the current line
1809 int line_no; //
1810 Brush disabled;
1811 Brush hilight;
1812 Brush hilight_text;
1814 // First, figure out from what line to what line we need to draw
1815 start = GetLineByPixel(clip.Top + viewport_y, false).line_no;
1816 end = GetLineByPixel(clip.Bottom + viewport_y, false).line_no;
1817 //Console.WriteLine("Starting drawing at line {0}, ending at line {1} (clip-bottom:{2})", start, end, clip.Bottom);
1819 // Now draw our elements; try to only draw those that are visible
1820 line_no = start;
1822 #if Debug
1823 DateTime n = DateTime.Now;
1824 Console.WriteLine("Started drawing: {0}s {1}ms", n.Second, n.Millisecond);
1825 #endif
1827 disabled = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorGrayText);
1828 hilight = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorHighlight);
1829 hilight_text = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorHighlightText);
1831 while (line_no <= end) {
1832 line = GetLine(line_no);
1833 #if not
1834 if (owner.backcolor_set || (owner.Enabled && !owner.read_only)) {
1835 g.FillRectangle(ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), new Rectangle(clip.Left, line.Y - viewport_y, clip.Width, line.Y - viewport_y + line.Height));
1836 } else {
1837 g.FillRectangle(ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorControl), new Rectangle(clip.Left, line.Y - viewport_y, clip.Width, line.Y - viewport_y + line.Height));
1839 #endif
1842 tag = line.tags;
1843 if (!calc_pass) {
1844 text = line.text;
1845 } else {
1846 // This fails if there's a password > 1024 chars...
1847 text = this.password_cache;
1849 while (tag != null) {
1850 if (tag.length == 0) {
1851 tag = tag.next;
1852 continue;
1855 if (((tag.X + tag.width) > (clip.Left - viewport_x)) || (tag.X < (clip.Right - viewport_x))) {
1856 // Check for selection
1857 if ((!selection_visible) || (!owner.ShowSelection) || (line_no < selection_start.line.line_no) || (line_no > selection_end.line.line_no)) {
1858 // regular drawing, no selection to deal with
1859 //g.DrawString(s.Substring(tag.start-1, tag.length), tag.font, tag.color, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1860 if (owner.is_enabled) {
1861 g.DrawString(text.ToString(tag.start-1, tag.length), tag.font, tag.color, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1862 } else {
1863 Color a;
1864 Color b;
1866 a = ((SolidBrush)tag.color).Color;
1867 b = ThemeEngine.Current.ColorWindowText;
1869 if ((a.R == b.R) && (a.G == b.G) && (a.B == b.B)) {
1870 g.DrawString(text.ToString(tag.start-1, tag.length), tag.font, disabled, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1871 } else {
1872 g.DrawString(text.ToString(tag.start-1, tag.length), tag.font, tag.color, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1875 } else {
1876 // we might have to draw our selection
1877 if ((line_no != selection_start.line.line_no) && (line_no != selection_end.line.line_no)) {
1878 // Special case, whole line is selected, draw this tag selected
1879 g.FillRectangle(
1880 hilight, // Brush
1881 tag.X + line.align_shift - viewport_x, // X
1882 line.Y + tag.shift - viewport_y, // Y
1883 line.widths[tag.start + tag.length - 1], // width
1884 tag.height // Height
1887 g.DrawString(
1888 //s.Substring(tag.start-1, tag.length), // String
1889 text.ToString(tag.start-1, tag.length), // String
1890 tag.font, // Font
1891 hilight_text, // Brush
1892 tag.X + line.align_shift - viewport_x, // X
1893 line.Y + tag.shift - viewport_y, // Y
1894 StringFormat.GenericTypographic);
1895 } else {
1896 bool highlight;
1897 bool partial;
1899 highlight = false;
1900 partial = false;
1902 // One or more, but not all tags on the line are selected
1903 if ((selection_start.tag == tag) && (selection_end.tag == tag)) {
1904 // Single tag selected, draw "normalSELECTEDnormal"
1905 partial = true;
1906 // First, the regular part
1907 g.DrawString(
1908 //s.Substring(tag.start - 1, selection_start.pos - tag.start + 1), // String
1909 text.ToString(tag.start - 1, selection_start.pos - tag.start + 1), // String
1910 tag.font, // Font
1911 tag.color, // Brush
1912 tag.X + line.align_shift - viewport_x, // X
1913 line.Y + tag.shift - viewport_y, // Y
1914 StringFormat.GenericTypographic);
1916 // Now the highlight
1917 g.FillRectangle(
1918 hilight, // Brush
1919 line.widths[selection_start.pos] + line.align_shift - viewport_x, // X
1920 line.Y + tag.shift - viewport_y, // Y
1921 line.widths[selection_end.pos] - line.widths[selection_start.pos], // Width
1922 tag.height); // Height
1924 g.DrawString(
1925 //s.Substring(selection_start.pos, selection_end.pos - selection_start.pos), // String
1926 text.ToString(selection_start.pos, selection_end.pos - selection_start.pos), // String
1927 tag.font, // Font
1928 hilight_text, // Brush
1929 line.widths[selection_start.pos] + line.align_shift - viewport_x, // X
1930 line.Y + tag.shift - viewport_y, // Y
1931 StringFormat.GenericTypographic);
1933 // And back to the regular
1934 g.DrawString(
1935 //s.Substring(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1936 text.ToString(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1937 tag.font, // Font
1938 tag.color, // Brush
1939 line.widths[selection_end.pos] + line.align_shift - viewport_x, // X
1940 line.Y + tag.shift - viewport_y, // Y
1941 StringFormat.GenericTypographic);
1943 } else if (selection_start.tag == tag) {
1944 partial = true;
1946 // The highlighted part
1947 g.FillRectangle(
1948 hilight,
1949 line.widths[selection_start.pos] + line.align_shift - viewport_x,
1950 line.Y + tag.shift - viewport_y,
1951 line.widths[tag.start + tag.length - 1] - line.widths[selection_start.pos],
1952 tag.height);
1954 g.DrawString(
1955 //s.Substring(selection_start.pos, tag.start + tag.length - selection_start.pos - 1), // String
1956 text.ToString(selection_start.pos, tag.start + tag.length - selection_start.pos - 1), // String
1957 tag.font, // Font
1958 hilight_text, // Brush
1959 line.widths[selection_start.pos] + line.align_shift - viewport_x, // X
1960 line.Y + tag.shift - viewport_y, // Y
1961 StringFormat.GenericTypographic);
1963 // The regular part
1964 g.DrawString(
1965 //s.Substring(tag.start - 1, selection_start.pos - tag.start + 1), // String
1966 text.ToString(tag.start - 1, selection_start.pos - tag.start + 1), // String
1967 tag.font, // Font
1968 tag.color, // Brush
1969 tag.X + line.align_shift - viewport_x, // X
1970 line.Y + tag.shift - viewport_y, // Y
1971 StringFormat.GenericTypographic);
1972 } else if (selection_end.tag == tag) {
1973 partial = true;
1975 // The highlighted part
1976 g.FillRectangle(
1977 hilight,
1978 tag.X + line.align_shift - viewport_x,
1979 line.Y + tag.shift - viewport_y,
1980 line.widths[selection_end.pos] - line.widths[tag.start - 1],
1981 tag.height);
1983 g.DrawString(
1984 //s.Substring(tag.start - 1, selection_end.pos - tag.start + 1), // String
1985 text.ToString(tag.start - 1, selection_end.pos - tag.start + 1), // String
1986 tag.font, // Font
1987 hilight_text, // Brush
1988 tag.X + line.align_shift - viewport_x, // X
1989 line.Y + tag.shift - viewport_y, // Y
1990 StringFormat.GenericTypographic);
1992 // The regular part
1993 g.DrawString(
1994 //s.Substring(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1995 text.ToString(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1996 tag.font, // Font
1997 tag.color, // Brush
1998 line.widths[selection_end.pos] + line.align_shift - viewport_x, // X
1999 line.Y + tag.shift - viewport_y, // Y
2000 StringFormat.GenericTypographic);
2001 } else {
2002 // no partially selected tags here, simple checks...
2003 if (selection_start.line == line) {
2004 int begin;
2005 int stop;
2007 begin = tag.start - 1;
2008 stop = tag.start + tag.length - 1;
2009 if (selection_end.line == line) {
2010 if ((begin >= selection_start.pos) && (stop < selection_end.pos)) {
2011 highlight = true;
2013 } else {
2014 if (stop > selection_start.pos) {
2015 highlight = true;
2018 } else if (selection_end.line == line) {
2019 if ((tag.start - 1) < selection_end.pos) {
2020 highlight = true;
2025 if (!partial) {
2026 if (highlight) {
2027 g.FillRectangle(
2028 hilight,
2029 tag.X + line.align_shift - viewport_x,
2030 line.Y + tag.shift - viewport_y,
2031 line.widths[tag.start + tag.length - 1] - line.widths[tag.start - 1],
2032 tag.height);
2034 g.DrawString(
2035 //s.Substring(tag.start-1, tag.length), // String
2036 text.ToString(tag.start-1, tag.length), // String
2037 tag.font, // Font
2038 hilight_text, // Brush
2039 tag.X + line.align_shift - viewport_x, // X
2040 line.Y + tag.shift - viewport_y, // Y
2041 StringFormat.GenericTypographic);
2042 } else {
2043 g.DrawString(
2044 //s.Substring(tag.start-1, tag.length), // String
2045 text.ToString(tag.start-1, tag.length), // String
2046 tag.font, // Font
2047 tag.color, // Brush
2048 tag.X + line.align_shift - viewport_x, // X
2049 line.Y + tag.shift - viewport_y, // Y
2050 StringFormat.GenericTypographic);
2058 tag = tag.next;
2061 line_no++;
2063 #if Debug
2064 n = DateTime.Now;
2065 Console.WriteLine("Finished drawing: {0}s {1}ms", n.Second, n.Millisecond);
2066 #endif
2070 internal void Insert(Line line, int pos, string s) {
2071 Insert(line, null, pos, false, s);
2074 // Insert multi-line text at the given position; use formatting at insertion point for inserted text
2075 internal void Insert(Line line, LineTag tag, int pos, bool update_caret, string s) {
2076 int i;
2077 int base_line;
2078 string[] ins;
2079 int insert_lines;
2080 int old_line_count;
2081 bool carriage_return = false;
2083 NoRecalc = true;
2085 // The formatting at the insertion point is used for the inserted text
2086 if (tag == null) {
2087 tag = LineTag.FindTag(line, pos);
2090 base_line = line.line_no;
2092 ins = s.Split(new char[] {'\n'});
2094 insert_lines = ins.Length;
2095 old_line_count = lines;
2097 // Bump the text at insertion point a line down if we're inserting more than one line
2098 if (insert_lines > 1) {
2099 Split(line, pos);
2100 line.soft_break = false;
2101 // Remainder of start line is now in base_line + 1
2104 if (ins [0].EndsWith ("\r")) {
2105 ins [0] = ins[0].Substring (0, ins[0].Length - 1);
2106 carriage_return = true;
2109 // Insert the first line
2110 InsertString(tag, pos, ins[0]);
2112 if (carriage_return) {
2113 Line l = GetLine (base_line);
2114 l.carriage_return = true;
2117 if (insert_lines > 1) {
2118 for (i = 1; i < insert_lines; i++) {
2119 carriage_return = false;
2120 if (ins [i].EndsWith ("\r")) {
2121 ins [i] = ins[i].Substring (0, ins[i].Length - 1);
2122 carriage_return = true;
2124 Add(base_line + i, ins[i], line.alignment, tag.font, tag.color);
2125 if (carriage_return) {
2126 Line l = GetLine (base_line + i);
2127 l.carriage_return = true;
2130 if (!s.EndsWith("\n\n")) {
2131 this.Combine(base_line + (lines - old_line_count) - 1, base_line + lines - old_line_count);
2135 NoRecalc = false;
2136 UpdateView(line, lines - old_line_count + 1, pos);
2138 if (update_caret) {
2139 // Move caret to the end of the inserted text
2140 if (insert_lines > 1) {
2141 Line l = GetLine (line.line_no + lines - old_line_count);
2142 PositionCaret(l, l.text.Length);
2143 } else {
2144 PositionCaret(line, pos + ins[0].Length);
2146 DisplayCaret ();
2150 // Inserts a character at the given position
2151 internal void InsertString(Line line, int pos, string s) {
2152 InsertString(line.FindTag(pos), pos, s);
2155 // Inserts a string at the given position
2156 internal void InsertString(LineTag tag, int pos, string s) {
2157 Line line;
2158 int len;
2160 len = s.Length;
2162 CharCount += len;
2164 line = tag.line;
2165 line.text.Insert(pos, s);
2166 tag.length += len;
2168 // TODO: sometimes getting a null tag here when pasting ???
2169 tag = tag.next;
2170 while (tag != null) {
2171 tag.start += len;
2172 tag = tag.next;
2174 line.Grow(len);
2175 line.recalc = true;
2177 UpdateView(line, pos);
2180 // Inserts a string at the caret position
2181 internal void InsertStringAtCaret(string s, bool move_caret) {
2182 LineTag tag;
2183 int len;
2185 len = s.Length;
2187 CharCount += len;
2189 caret.line.text.Insert(caret.pos, s);
2190 caret.tag.length += len;
2192 if (caret.tag.next != null) {
2193 tag = caret.tag.next;
2194 while (tag != null) {
2195 tag.start += len;
2196 tag = tag.next;
2199 caret.line.Grow(len);
2200 caret.line.recalc = true;
2202 UpdateView(caret.line, caret.pos);
2203 if (move_caret) {
2204 caret.pos += len;
2205 UpdateCaret();
2211 // Inserts a character at the given position
2212 internal void InsertChar(Line line, int pos, char ch) {
2213 InsertChar(line.FindTag(pos), pos, ch);
2216 // Inserts a character at the given position
2217 internal void InsertChar(LineTag tag, int pos, char ch) {
2218 Line line;
2220 CharCount++;
2222 line = tag.line;
2223 line.text.Insert(pos, ch);
2224 tag.length++;
2226 tag = tag.next;
2227 while (tag != null) {
2228 tag.start++;
2229 tag = tag.next;
2231 line.Grow(1);
2232 line.recalc = true;
2234 UpdateView(line, pos);
2237 // Inserts a character at the current caret position
2238 internal void InsertCharAtCaret(char ch, bool move_caret) {
2239 LineTag tag;
2241 CharCount++;
2243 caret.line.text.Insert(caret.pos, ch);
2244 caret.tag.length++;
2246 if (caret.tag.next != null) {
2247 tag = caret.tag.next;
2248 while (tag != null) {
2249 tag.start++;
2250 tag = tag.next;
2253 caret.line.Grow(1);
2254 caret.line.recalc = true;
2256 UpdateView(caret.line, caret.pos);
2257 if (move_caret) {
2258 caret.pos++;
2259 UpdateCaret();
2260 SetSelectionToCaret(true);
2264 internal void DeleteMultiline (Line start_line, int pos, int length)
2266 Marker start = new Marker ();
2267 Marker end = new Marker ();
2268 int start_index = LineTagToCharIndex (start_line, pos);
2270 start.line = start_line;
2271 start.pos = pos;
2272 start.tag = LineTag.FindTag (start_line, pos);
2274 CharIndexToLineTag (start_index + length, out end.line,
2275 out end.tag, out end.pos);
2277 if (start.line == end.line) {
2278 DeleteChars (start.tag, pos, end.pos - pos);
2279 } else {
2281 // Delete first and last lines
2282 DeleteChars (start.tag, start.pos, start.line.text.Length - start.pos);
2283 DeleteChars (end.line.tags, 0, end.pos);
2285 int current = start.line.line_no + 1;
2286 if (current < end.line.line_no) {
2287 for (int i = end.line.line_no - 1; i >= current; i--) {
2288 Delete (i);
2292 // BIG FAT WARNING - selection_end.line might be stale due
2293 // to the above Delete() call. DONT USE IT before hitting the end of this method!
2295 // Join start and end
2296 Combine (start.line.line_no, current);
2301 // Deletes n characters at the given position; it will not delete past line limits
2302 // pos is 0-based
2303 internal void DeleteChars(LineTag tag, int pos, int count) {
2304 Line line;
2305 bool streamline;
2307 streamline = false;
2308 line = tag.line;
2310 CharCount -= count;
2312 if (pos == line.text.Length) {
2313 return;
2316 line.text.Remove(pos, count);
2318 // Make sure the tag points to the right spot
2319 while ((tag != null) && (tag.start + tag.length - 1) <= pos) {
2320 tag = tag.next;
2323 if (tag == null) {
2324 return;
2327 // Check if we're crossing tag boundaries
2328 if ((pos + count) > (tag.start + tag.length - 1)) {
2329 int left;
2331 // We have to delete cross tag boundaries
2332 streamline = true;
2333 left = count;
2335 left -= tag.start + tag.length - pos - 1;
2336 tag.length -= tag.start + tag.length - pos - 1;
2338 tag = tag.next;
2339 while ((tag != null) && (left > 0)) {
2340 tag.start -= count - left;
2341 if (tag.length > left) {
2342 tag.length -= left;
2343 left = 0;
2344 } else {
2345 left -= tag.length;
2346 tag.length = 0;
2348 tag = tag.next;
2351 } else {
2352 // We got off easy, same tag
2354 tag.length -= count;
2356 if (tag.length == 0) {
2357 streamline = true;
2361 // Delete empty orphaned tags at the end
2362 LineTag walk = tag;
2363 while (walk != null && walk.next != null && walk.next.length == 0) {
2364 LineTag t = walk;
2365 walk.next = walk.next.next;
2366 if (walk.next != null)
2367 walk.next.previous = t;
2368 walk = walk.next;
2371 // Adjust the start point of any tags following
2372 if (tag != null) {
2373 tag = tag.next;
2374 while (tag != null) {
2375 tag.start -= count;
2376 tag = tag.next;
2380 line.recalc = true;
2381 if (streamline) {
2382 line.Streamline(lines);
2385 UpdateView(line, pos);
2388 // Deletes a character at or after the given position (depending on forward); it will not delete past line limits
2389 internal void DeleteChar(LineTag tag, int pos, bool forward) {
2390 Line line;
2391 bool streamline;
2393 CharCount--;
2395 streamline = false;
2396 line = tag.line;
2398 if ((pos == 0 && forward == false) || (pos == line.text.Length && forward == true)) {
2399 return;
2403 if (forward) {
2404 line.text.Remove(pos, 1);
2406 while ((tag != null) && (tag.start + tag.length - 1) <= pos) {
2407 tag = tag.next;
2410 if (tag == null) {
2411 return;
2414 tag.length--;
2416 if (tag.length == 0) {
2417 streamline = true;
2419 } else {
2420 pos--;
2421 line.text.Remove(pos, 1);
2422 if (pos >= (tag.start - 1)) {
2423 tag.length--;
2424 if (tag.length == 0) {
2425 streamline = true;
2427 } else if (tag.previous != null) {
2428 tag.previous.length--;
2429 if (tag.previous.length == 0) {
2430 streamline = true;
2435 // Delete empty orphaned tags at the end
2436 LineTag walk = tag;
2437 while (walk != null && walk.next != null && walk.next.length == 0) {
2438 LineTag t = walk;
2439 walk.next = walk.next.next;
2440 if (walk.next != null)
2441 walk.next.previous = t;
2442 walk = walk.next;
2445 tag = tag.next;
2446 while (tag != null) {
2447 tag.start--;
2448 tag = tag.next;
2450 line.recalc = true;
2451 if (streamline) {
2452 line.Streamline(lines);
2455 UpdateView(line, pos);
2458 // Combine two lines
2459 internal void Combine(int FirstLine, int SecondLine) {
2460 Combine(GetLine(FirstLine), GetLine(SecondLine));
2463 internal void Combine(Line first, Line second) {
2464 LineTag last;
2465 int shift;
2467 // Combine the two tag chains into one
2468 last = first.tags;
2470 // Maintain the line ending style
2471 first.soft_break = second.soft_break;
2473 while (last.next != null) {
2474 last = last.next;
2477 last.next = second.tags;
2478 last.next.previous = last;
2480 shift = last.start + last.length - 1;
2482 // Fix up references within the chain
2483 last = last.next;
2484 while (last != null) {
2485 last.line = first;
2486 last.start += shift;
2487 last = last.next;
2490 // Combine both lines' strings
2491 first.text.Insert(first.text.Length, second.text.ToString());
2492 first.Grow(first.text.Length);
2494 // Remove the reference to our (now combined) tags from the doomed line
2495 second.tags = null;
2497 // Renumber lines
2498 DecrementLines(first.line_no + 2); // first.line_no + 1 will be deleted, so we need to start renumbering one later
2500 // Mop up
2501 first.recalc = true;
2502 first.height = 0; // This forces RecalcDocument/UpdateView to redraw from this line on
2503 first.Streamline(lines);
2505 // Update Caret, Selection, etc
2506 if (caret.line == second) {
2507 caret.Combine(first, shift);
2509 if (selection_anchor.line == second) {
2510 selection_anchor.Combine(first, shift);
2512 if (selection_start.line == second) {
2513 selection_start.Combine(first, shift);
2515 if (selection_end.line == second) {
2516 selection_end.Combine(first, shift);
2519 #if Debug
2520 Line check_first;
2521 Line check_second;
2523 check_first = GetLine(first.line_no);
2524 check_second = GetLine(check_first.line_no + 1);
2526 Console.WriteLine("Pre-delete: Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
2527 #endif
2529 this.Delete(second);
2531 #if Debug
2532 check_first = GetLine(first.line_no);
2533 check_second = GetLine(check_first.line_no + 1);
2535 Console.WriteLine("Post-delete Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
2536 #endif
2539 // Split the line at the position into two
2540 internal void Split(int LineNo, int pos) {
2541 Line line;
2542 LineTag tag;
2544 line = GetLine(LineNo);
2545 tag = LineTag.FindTag(line, pos);
2546 Split(line, tag, pos, false);
2549 internal void Split(Line line, int pos) {
2550 LineTag tag;
2552 tag = LineTag.FindTag(line, pos);
2553 Split(line, tag, pos, false);
2556 ///<summary>Split line at given tag and position into two lines</summary>
2557 ///<param name="soft">True if the split should be marked as 'soft', indicating that it can be contracted
2558 ///if more space becomes available on previous line</param>
2559 internal void Split(Line line, LineTag tag, int pos, bool soft) {
2560 LineTag new_tag;
2561 Line new_line;
2562 bool move_caret;
2563 bool move_sel_start;
2564 bool move_sel_end;
2566 move_caret = false;
2567 move_sel_start = false;
2568 move_sel_end = false;
2570 // Adjust selection and cursors
2571 if (soft && (caret.line == line) && (caret.pos >= pos)) {
2572 move_caret = true;
2574 if (selection_start.line == line && selection_start.pos > pos) {
2575 move_sel_start = true;
2578 if (selection_end.line == line && selection_end.pos > pos) {
2579 move_sel_end = true;
2582 // cover the easy case first
2583 if (pos == line.text.Length) {
2584 Add(line.line_no + 1, "", line.alignment, tag.font, tag.color);
2586 new_line = GetLine(line.line_no + 1);
2588 line.carriage_return = false;
2589 new_line.carriage_return = line.carriage_return;
2591 if (soft) {
2592 if (move_caret) {
2593 caret.line = new_line;
2594 caret.line.soft_break = true;
2595 caret.tag = new_line.tags;
2596 caret.pos = 0;
2597 } else {
2598 new_line.soft_break = true;
2602 if (move_sel_start) {
2603 selection_start.line = new_line;
2604 selection_start.pos = 0;
2605 selection_start.tag = new_line.tags;
2608 if (move_sel_end) {
2609 selection_end.line = new_line;
2610 selection_end.pos = 0;
2611 selection_end.tag = new_line.tags;
2613 return;
2616 // We need to move the rest of the text into the new line
2617 Add(line.line_no + 1, line.text.ToString(pos, line.text.Length - pos), line.alignment, tag.font, tag.color);
2619 // Now transfer our tags from this line to the next
2620 new_line = GetLine(line.line_no + 1);
2622 line.carriage_return = false;
2623 new_line.carriage_return = line.carriage_return;
2625 line.recalc = true;
2626 new_line.recalc = true;
2628 if ((tag.start - 1) == pos) {
2629 int shift;
2631 // We can simply break the chain and move the tag into the next line
2632 if (tag == line.tags) {
2633 new_tag = new LineTag(line, 1, 0);
2634 new_tag.font = tag.font;
2635 new_tag.color = tag.color;
2636 line.tags = new_tag;
2639 if (tag.previous != null) {
2640 tag.previous.next = null;
2642 new_line.tags = tag;
2643 tag.previous = null;
2644 tag.line = new_line;
2646 // Walk the list and correct the start location of the tags we just bumped into the next line
2647 shift = tag.start - 1;
2649 new_tag = tag;
2650 while (new_tag != null) {
2651 new_tag.start -= shift;
2652 new_tag.line = new_line;
2653 new_tag = new_tag.next;
2655 } else {
2656 int shift;
2658 new_tag = new LineTag(new_line, 1, tag.start - 1 + tag.length - pos);
2659 new_tag.next = tag.next;
2660 new_tag.font = tag.font;
2661 new_tag.color = tag.color;
2662 new_line.tags = new_tag;
2663 if (new_tag.next != null) {
2664 new_tag.next.previous = new_tag;
2666 tag.next = null;
2667 tag.length = pos - tag.start + 1;
2669 shift = pos;
2670 new_tag = new_tag.next;
2671 while (new_tag != null) {
2672 new_tag.start -= shift;
2673 new_tag.line = new_line;
2674 new_tag = new_tag.next;
2679 if (soft) {
2680 if (move_caret) {
2681 caret.line = new_line;
2682 caret.pos = caret.pos - pos;
2683 caret.tag = caret.line.FindTag(caret.pos);
2685 new_line.soft_break = true;
2688 if (move_sel_start) {
2689 selection_start.line = new_line;
2690 selection_start.pos = selection_start.pos - pos;
2691 selection_start.tag = new_line.FindTag(selection_start.pos);
2694 if (move_sel_end) {
2695 selection_end.line = new_line;
2696 selection_end.pos = selection_end.pos - pos;
2697 selection_end.tag = new_line.FindTag(selection_end.pos);
2700 CharCount -= line.text.Length - pos;
2701 line.text.Remove(pos, line.text.Length - pos);
2704 // Adds a line of text, with given font.
2705 // Bumps any line at that line number that already exists down
2706 internal void Add(int LineNo, string Text, Font font, Brush color) {
2707 Add(LineNo, Text, HorizontalAlignment.Left, font, color);
2710 internal void Add(int LineNo, string Text, HorizontalAlignment align, Font font, Brush color) {
2711 Line add;
2712 Line line;
2713 int line_no;
2715 CharCount += Text.Length;
2717 if (LineNo<1 || Text == null) {
2718 if (LineNo<1) {
2719 throw new ArgumentNullException("LineNo", "Line numbers must be positive");
2720 } else {
2721 throw new ArgumentNullException("Text", "Cannot insert NULL line");
2725 add = new Line(LineNo, Text, align, font, color);
2727 line = document;
2728 while (line != sentinel) {
2729 add.parent = line;
2730 line_no = line.line_no;
2732 if (LineNo > line_no) {
2733 line = line.right;
2734 } else if (LineNo < line_no) {
2735 line = line.left;
2736 } else {
2737 // Bump existing line numbers; walk all nodes to the right of this one and increment line_no
2738 IncrementLines(line.line_no);
2739 line = line.left;
2743 add.left = sentinel;
2744 add.right = sentinel;
2746 if (add.parent != null) {
2747 if (LineNo > add.parent.line_no) {
2748 add.parent.right = add;
2749 } else {
2750 add.parent.left = add;
2752 } else {
2753 // Root node
2754 document = add;
2757 RebalanceAfterAdd(add);
2759 lines++;
2762 internal virtual void Clear() {
2763 lines = 0;
2764 CharCount = 0;
2765 document = sentinel;
2768 public virtual object Clone() {
2769 Document clone;
2771 clone = new Document(null);
2773 clone.lines = this.lines;
2774 clone.document = (Line)document.Clone();
2776 return clone;
2779 internal void Delete(int LineNo) {
2780 Line line;
2782 if (LineNo>lines) {
2783 return;
2786 line = GetLine(LineNo);
2788 CharCount -= line.text.Length;
2790 DecrementLines(LineNo + 1);
2791 Delete(line);
2794 internal void Delete(Line line1) {
2795 Line line2;// = new Line();
2796 Line line3;
2798 if ((line1.left == sentinel) || (line1.right == sentinel)) {
2799 line3 = line1;
2800 } else {
2801 line3 = line1.right;
2802 while (line3.left != sentinel) {
2803 line3 = line3.left;
2807 if (line3.left != sentinel) {
2808 line2 = line3.left;
2809 } else {
2810 line2 = line3.right;
2813 line2.parent = line3.parent;
2814 if (line3.parent != null) {
2815 if(line3 == line3.parent.left) {
2816 line3.parent.left = line2;
2817 } else {
2818 line3.parent.right = line2;
2820 } else {
2821 document = line2;
2824 if (line3 != line1) {
2825 LineTag tag;
2827 if (selection_start.line == line3) {
2828 selection_start.line = line1;
2831 if (selection_end.line == line3) {
2832 selection_end.line = line1;
2835 if (selection_anchor.line == line3) {
2836 selection_anchor.line = line1;
2839 if (caret.line == line3) {
2840 caret.line = line1;
2844 line1.alignment = line3.alignment;
2845 line1.ascent = line3.ascent;
2846 line1.hanging_indent = line3.hanging_indent;
2847 line1.height = line3.height;
2848 line1.indent = line3.indent;
2849 line1.line_no = line3.line_no;
2850 line1.recalc = line3.recalc;
2851 line1.right_indent = line3.right_indent;
2852 line1.soft_break = line3.soft_break;
2853 line1.space = line3.space;
2854 line1.tags = line3.tags;
2855 line1.text = line3.text;
2856 line1.widths = line3.widths;
2857 line1.Y = line3.Y;
2859 tag = line1.tags;
2860 while (tag != null) {
2861 tag.line = line1;
2862 tag = tag.next;
2866 if (line3.color == LineColor.Black)
2867 RebalanceAfterDelete(line2);
2869 this.lines--;
2872 // Invalidate a section of the document to trigger redraw
2873 internal void Invalidate(Line start, int start_pos, Line end, int end_pos) {
2874 Line l1;
2875 Line l2;
2876 int p1;
2877 int p2;
2879 if ((start == end) && (start_pos == end_pos)) {
2880 return;
2883 if (end_pos == -1) {
2884 end_pos = end.text.Length;
2887 // figure out what's before what so the logic below is straightforward
2888 if (start.line_no < end.line_no) {
2889 l1 = start;
2890 p1 = start_pos;
2892 l2 = end;
2893 p2 = end_pos;
2894 } else if (start.line_no > end.line_no) {
2895 l1 = end;
2896 p1 = end_pos;
2898 l2 = start;
2899 p2 = start_pos;
2900 } else {
2901 if (start_pos < end_pos) {
2902 l1 = start;
2903 p1 = start_pos;
2905 l2 = end;
2906 p2 = end_pos;
2907 } else {
2908 l1 = end;
2909 p1 = end_pos;
2911 l2 = start;
2912 p2 = start_pos;
2915 #if Debug
2916 Console.WriteLine("Invaliding from {0}:{1} to {2}:{3}", l1.line_no, p1, l2.line_no, p2);
2917 #endif
2919 owner.Invalidate(
2920 new Rectangle(
2921 (int)l1.widths[p1] + l1.align_shift - viewport_x,
2922 l1.Y - viewport_y,
2923 (int)l2.widths[p2] - (int)l1.widths[p1] + 1,
2924 l1.height
2927 return;
2930 #if Debug
2931 Console.WriteLine("Invaliding from {0}:{1} to {2}:{3} Start => x={4}, y={5}, {6}x{7}", l1.line_no, p1, l2.line_no, p2, (int)l1.widths[p1] + l1.align_shift - viewport_x, l1.Y - viewport_y, viewport_width, l1.height);
2932 #endif
2934 // Three invalidates:
2935 // First line from start
2936 owner.Invalidate(new Rectangle((int)l1.widths[p1] + l1.align_shift - viewport_x, l1.Y - viewport_y, viewport_width, l1.height));
2938 // lines inbetween
2939 if ((l1.line_no + 1) < l2.line_no) {
2940 int y;
2942 y = GetLine(l1.line_no + 1).Y;
2943 owner.Invalidate(new Rectangle(0, y - viewport_y, viewport_width, GetLine(l2.line_no).Y - y - viewport_y));
2945 #if Debug
2946 Console.WriteLine("Invaliding from {0}:{1} to {2}:{3} Middle => x={4}, y={5}, {6}x{7}", l1.line_no, p1, l2.line_no, p2, 0, y - viewport_y, viewport_width, GetLine(l2.line_no).Y - y - viewport_y);
2947 #endif
2950 // Last line to end
2951 owner.Invalidate(new Rectangle((int)l2.widths[0] + l2.align_shift - viewport_x, l2.Y - viewport_y, (int)l2.widths[p2] + 1, l2.height));
2952 #if Debug
2953 Console.WriteLine("Invaliding from {0}:{1} to {2}:{3} End => x={4}, y={5}, {6}x{7}", l1.line_no, p1, l2.line_no, p2, (int)l2.widths[0] + l2.align_shift - viewport_x, l2.Y - viewport_y, (int)l2.widths[p2] + 1, l2.height);
2954 #endif
2957 /// <summary>Select text around caret</summary>
2958 internal void ExpandSelection(CaretSelection mode, bool to_caret) {
2959 if (to_caret) {
2960 // We're expanding the selection to the caret position
2961 switch(mode) {
2962 case CaretSelection.Line: {
2963 // Invalidate the selection delta
2964 if (caret > selection_prev) {
2965 Invalidate(selection_prev.line, 0, caret.line, caret.line.text.Length);
2966 } else {
2967 Invalidate(selection_prev.line, selection_prev.line.text.Length, caret.line, 0);
2970 if (caret.line.line_no <= selection_anchor.line.line_no) {
2971 selection_start.line = caret.line;
2972 selection_start.tag = caret.line.tags;
2973 selection_start.pos = 0;
2975 selection_end.line = selection_anchor.line;
2976 selection_end.tag = selection_anchor.tag;
2977 selection_end.pos = selection_anchor.pos;
2979 selection_end_anchor = true;
2980 } else {
2981 selection_start.line = selection_anchor.line;
2982 selection_start.pos = selection_anchor.height;
2983 selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height);
2985 selection_end.line = caret.line;
2986 selection_end.tag = caret.line.tags;
2987 selection_end.pos = caret.line.text.Length;
2989 selection_end_anchor = false;
2991 selection_prev.line = caret.line;
2992 selection_prev.tag = caret.tag;
2993 selection_prev.pos = caret.pos;
2995 break;
2998 case CaretSelection.Word: {
2999 int start_pos;
3000 int end_pos;
3002 start_pos = FindWordSeparator(caret.line, caret.pos, false);
3003 end_pos = FindWordSeparator(caret.line, caret.pos, true);
3006 // Invalidate the selection delta
3007 if (caret > selection_prev) {
3008 Invalidate(selection_prev.line, selection_prev.pos, caret.line, end_pos);
3009 } else {
3010 Invalidate(selection_prev.line, selection_prev.pos, caret.line, start_pos);
3012 if (caret < selection_anchor) {
3013 selection_start.line = caret.line;
3014 selection_start.tag = caret.line.FindTag(start_pos);
3015 selection_start.pos = start_pos;
3017 selection_end.line = selection_anchor.line;
3018 selection_end.tag = selection_anchor.tag;
3019 selection_end.pos = selection_anchor.pos;
3021 selection_prev.line = caret.line;
3022 selection_prev.tag = caret.tag;
3023 selection_prev.pos = start_pos;
3025 selection_end_anchor = true;
3026 } else {
3027 selection_start.line = selection_anchor.line;
3028 selection_start.pos = selection_anchor.height;
3029 selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height);
3031 selection_end.line = caret.line;
3032 selection_end.tag = caret.line.FindTag(end_pos);
3033 selection_end.pos = end_pos;
3035 selection_prev.line = caret.line;
3036 selection_prev.tag = caret.tag;
3037 selection_prev.pos = end_pos;
3039 selection_end_anchor = false;
3041 break;
3044 case CaretSelection.Position: {
3045 SetSelectionToCaret(false);
3046 return;
3049 } else {
3050 // We're setting the selection 'around' the caret position
3051 switch(mode) {
3052 case CaretSelection.Line: {
3053 this.Invalidate(caret.line, 0, caret.line, caret.line.text.Length);
3055 selection_start.line = caret.line;
3056 selection_start.tag = caret.line.tags;
3057 selection_start.pos = 0;
3059 selection_end.line = caret.line;
3060 selection_end.pos = caret.line.text.Length;
3061 selection_end.tag = caret.line.FindTag(selection_end.pos);
3063 selection_anchor.line = selection_end.line;
3064 selection_anchor.tag = selection_end.tag;
3065 selection_anchor.pos = selection_end.pos;
3066 selection_anchor.height = 0;
3068 selection_prev.line = caret.line;
3069 selection_prev.tag = caret.tag;
3070 selection_prev.pos = caret.pos;
3072 this.selection_end_anchor = true;
3074 break;
3077 case CaretSelection.Word: {
3078 int start_pos;
3079 int end_pos;
3081 start_pos = FindWordSeparator(caret.line, caret.pos, false);
3082 end_pos = FindWordSeparator(caret.line, caret.pos, true);
3084 this.Invalidate(selection_start.line, start_pos, caret.line, end_pos);
3086 selection_start.line = caret.line;
3087 selection_start.tag = caret.line.FindTag(start_pos);
3088 selection_start.pos = start_pos;
3090 selection_end.line = caret.line;
3091 selection_end.tag = caret.line.FindTag(end_pos);
3092 selection_end.pos = end_pos;
3094 selection_anchor.line = selection_end.line;
3095 selection_anchor.tag = selection_end.tag;
3096 selection_anchor.pos = selection_end.pos;
3097 selection_anchor.height = start_pos;
3099 selection_prev.line = caret.line;
3100 selection_prev.tag = caret.tag;
3101 selection_prev.pos = caret.pos;
3103 this.selection_end_anchor = true;
3105 break;
3110 SetSelectionVisible (!(selection_start == selection_end));
3113 internal void SetSelectionToCaret(bool start) {
3114 if (start) {
3115 // Invalidate old selection; selection is being reset to empty
3116 this.Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3118 selection_start.line = caret.line;
3119 selection_start.tag = caret.tag;
3120 selection_start.pos = caret.pos;
3122 // start always also selects end
3123 selection_end.line = caret.line;
3124 selection_end.tag = caret.tag;
3125 selection_end.pos = caret.pos;
3127 selection_anchor.line = caret.line;
3128 selection_anchor.tag = caret.tag;
3129 selection_anchor.pos = caret.pos;
3130 } else {
3131 // Invalidate from previous end to caret (aka new end)
3132 if (selection_end_anchor) {
3133 if (selection_start != caret) {
3134 this.Invalidate(selection_start.line, selection_start.pos, caret.line, caret.pos);
3136 } else {
3137 if (selection_end != caret) {
3138 this.Invalidate(selection_end.line, selection_end.pos, caret.line, caret.pos);
3142 if (caret < selection_anchor) {
3143 selection_start.line = caret.line;
3144 selection_start.tag = caret.tag;
3145 selection_start.pos = caret.pos;
3147 selection_end.line = selection_anchor.line;
3148 selection_end.tag = selection_anchor.tag;
3149 selection_end.pos = selection_anchor.pos;
3151 selection_end_anchor = true;
3152 } else {
3153 selection_start.line = selection_anchor.line;
3154 selection_start.tag = selection_anchor.tag;
3155 selection_start.pos = selection_anchor.pos;
3157 selection_end.line = caret.line;
3158 selection_end.tag = caret.tag;
3159 selection_end.pos = caret.pos;
3161 selection_end_anchor = false;
3165 SetSelectionVisible (!(selection_start == selection_end));
3168 internal void SetSelection(Line start, int start_pos, Line end, int end_pos) {
3169 if (selection_visible) {
3170 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3173 if ((end.line_no < start.line_no) || ((end == start) && (end_pos <= start_pos))) {
3174 selection_start.line = end;
3175 selection_start.tag = LineTag.FindTag(end, end_pos);
3176 selection_start.pos = end_pos;
3178 selection_end.line = start;
3179 selection_end.tag = LineTag.FindTag(start, start_pos);
3180 selection_end.pos = start_pos;
3182 selection_end_anchor = true;
3183 } else {
3184 selection_start.line = start;
3185 selection_start.tag = LineTag.FindTag(start, start_pos);
3186 selection_start.pos = start_pos;
3188 selection_end.line = end;
3189 selection_end.tag = LineTag.FindTag(end, end_pos);
3190 selection_end.pos = end_pos;
3192 selection_end_anchor = false;
3195 selection_anchor.line = start;
3196 selection_anchor.tag = selection_start.tag;
3197 selection_anchor.pos = start_pos;
3199 if (((start == end) && (start_pos == end_pos)) || start == null || end == null) {
3200 SetSelectionVisible (false);
3201 } else {
3202 SetSelectionVisible (true);
3203 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3207 internal void SetSelectionStart(Line start, int start_pos) {
3208 // Invalidate from the previous to the new start pos
3209 Invalidate(selection_start.line, selection_start.pos, start, start_pos);
3211 selection_start.line = start;
3212 selection_start.pos = start_pos;
3213 selection_start.tag = LineTag.FindTag(start, start_pos);
3215 selection_anchor.line = start;
3216 selection_anchor.pos = start_pos;
3217 selection_anchor.tag = selection_start.tag;
3219 selection_end_anchor = false;
3222 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
3223 SetSelectionVisible (true);
3224 } else {
3225 SetSelectionVisible (false);
3228 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3231 internal void SetSelectionStart(int character_index) {
3232 Line line;
3233 LineTag tag;
3234 int pos;
3236 if (character_index < 0) {
3237 return;
3240 CharIndexToLineTag(character_index, out line, out tag, out pos);
3241 SetSelectionStart(line, pos);
3244 internal void SetSelectionEnd(Line end, int end_pos) {
3246 if (end == selection_end.line && end_pos == selection_start.pos) {
3247 selection_anchor.line = selection_start.line;
3248 selection_anchor.tag = selection_start.tag;
3249 selection_anchor.pos = selection_start.pos;
3251 selection_end.line = selection_start.line;
3252 selection_end.tag = selection_start.tag;
3253 selection_end.pos = selection_start.pos;
3255 selection_end_anchor = false;
3256 } else if ((end.line_no < selection_anchor.line.line_no) || ((end == selection_anchor.line) && (end_pos <= selection_anchor.pos))) {
3257 selection_start.line = end;
3258 selection_start.tag = LineTag.FindTag(end, end_pos);
3259 selection_start.pos = end_pos;
3261 selection_end.line = selection_anchor.line;
3262 selection_end.tag = selection_anchor.tag;
3263 selection_end.pos = selection_anchor.pos;
3265 selection_end_anchor = true;
3266 } else {
3267 selection_start.line = selection_anchor.line;
3268 selection_start.tag = selection_anchor.tag;
3269 selection_start.pos = selection_anchor.pos;
3271 selection_end.line = end;
3272 selection_end.tag = LineTag.FindTag(end, end_pos);
3273 selection_end.pos = end_pos;
3275 selection_end_anchor = false;
3278 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
3279 SetSelectionVisible (true);
3280 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3281 } else {
3282 SetSelectionVisible (false);
3283 // ?? Do I need to invalidate here, tests seem to work without it, but I don't think they should :-s
3287 internal void SetSelectionEnd(int character_index) {
3288 Line line;
3289 LineTag tag;
3290 int pos;
3292 if (character_index < 0) {
3293 return;
3296 CharIndexToLineTag(character_index, out line, out tag, out pos);
3297 SetSelectionEnd(line, pos);
3300 internal void SetSelection(Line start, int start_pos) {
3301 if (selection_visible) {
3302 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3305 selection_start.line = start;
3306 selection_start.pos = start_pos;
3307 selection_start.tag = LineTag.FindTag(start, start_pos);
3309 selection_end.line = start;
3310 selection_end.tag = selection_start.tag;
3311 selection_end.pos = start_pos;
3313 selection_anchor.line = start;
3314 selection_anchor.tag = selection_start.tag;
3315 selection_anchor.pos = start_pos;
3317 selection_end_anchor = false;
3318 SetSelectionVisible (false);
3321 internal void InvalidateSelectionArea() {
3322 Invalidate (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3325 // Return the current selection, as string
3326 internal string GetSelection() {
3327 // We return String.Empty if there is no selection
3328 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
3329 return string.Empty;
3332 if (!multiline || (selection_start.line == selection_end.line)) {
3333 return selection_start.line.text.ToString(selection_start.pos, selection_end.pos - selection_start.pos);
3334 } else {
3335 StringBuilder sb;
3336 int i;
3337 int start;
3338 int end;
3340 sb = new StringBuilder();
3341 start = selection_start.line.line_no;
3342 end = selection_end.line.line_no;
3344 sb.Append(selection_start.line.text.ToString(selection_start.pos, selection_start.line.text.Length - selection_start.pos) + Environment.NewLine);
3346 if ((start + 1) < end) {
3347 for (i = start + 1; i < end; i++) {
3348 sb.Append(GetLine(i).text.ToString() + Environment.NewLine);
3352 sb.Append(selection_end.line.text.ToString(0, selection_end.pos));
3354 return sb.ToString();
3358 internal void ReplaceSelection(string s, bool select_new) {
3359 int i;
3361 undo.BeginCompoundAction ();
3363 int selection_start_pos = LineTagToCharIndex (selection_start.line, selection_start.pos);
3364 // First, delete any selected text
3365 if ((selection_start.pos != selection_end.pos) || (selection_start.line != selection_end.line)) {
3366 if (!multiline || (selection_start.line == selection_end.line)) {
3367 undo.RecordDeleteChars(selection_start.line, selection_start.pos + 1, selection_end.pos - selection_start.pos);
3369 DeleteChars(selection_start.tag, selection_start.pos, selection_end.pos - selection_start.pos);
3371 // The tag might have been removed, we need to recalc it
3372 selection_start.tag = selection_start.line.FindTag(selection_start.pos);
3373 } else {
3374 int start;
3375 int end;
3377 start = selection_start.line.line_no;
3378 end = selection_end.line.line_no;
3380 undo.RecordDelete(selection_start.line, selection_start.pos + 1, selection_end.line, selection_end.pos);
3382 // Delete first line
3383 DeleteChars(selection_start.tag, selection_start.pos, selection_start.line.text.Length - selection_start.pos);
3385 // Delete last line
3386 DeleteChars(selection_end.line.tags, 0, selection_end.pos);
3388 start++;
3389 if (start < end) {
3390 for (i = end - 1; i >= start; i--) {
3391 Delete(i);
3395 // BIG FAT WARNING - selection_end.line might be stale due
3396 // to the above Delete() call. DONT USE IT before hitting the end of this method!
3398 // Join start and end
3399 Combine(selection_start.line.line_no, start);
3403 Insert(selection_start.line, null, selection_start.pos, true, s);
3404 undo.RecordInsertString (selection_start.line, selection_start.pos, s);
3406 if (!select_new) {
3407 CharIndexToLineTag(selection_start_pos + s.Length, out selection_start.line,
3408 out selection_start.tag, out selection_start.pos);
3410 selection_end.line = selection_start.line;
3411 selection_end.pos = selection_start.pos;
3412 selection_end.tag = selection_start.tag;
3413 selection_anchor.line = selection_start.line;
3414 selection_anchor.pos = selection_start.pos;
3415 selection_anchor.tag = selection_start.tag;
3417 SetSelectionVisible (false);
3418 } else {
3419 CharIndexToLineTag(selection_start_pos, out selection_start.line,
3420 out selection_start.tag, out selection_start.pos);
3422 CharIndexToLineTag(selection_start_pos + s.Length, out selection_end.line,
3423 out selection_end.tag, out selection_end.pos);
3425 selection_anchor.line = selection_start.line;
3426 selection_anchor.pos = selection_start.pos;
3427 selection_anchor.tag = selection_start.tag;
3429 SetSelectionVisible (true);
3432 undo.EndCompoundAction ();
3435 internal void CharIndexToLineTag(int index, out Line line_out, out LineTag tag_out, out int pos) {
3436 Line line;
3437 LineTag tag;
3438 int i;
3439 int chars;
3440 int start;
3442 chars = 0;
3444 for (i = 1; i <= lines; i++) {
3445 line = GetLine(i);
3447 start = chars;
3448 chars += line.text.Length + crlf_size;
3450 if (index <= chars) {
3451 // we found the line
3452 tag = line.tags;
3454 while (tag != null) {
3455 if (index < (start + tag.start + tag.length)) {
3456 line_out = line;
3457 tag_out = LineTag.GetFinalTag (tag);
3458 pos = index - start;
3459 return;
3461 if (tag.next == null) {
3462 Line next_line;
3464 next_line = GetLine(line.line_no + 1);
3466 if (next_line != null) {
3467 line_out = next_line;
3468 tag_out = LineTag.GetFinalTag (next_line.tags);
3469 pos = 0;
3470 return;
3471 } else {
3472 line_out = line;
3473 tag_out = LineTag.GetFinalTag (tag);
3474 pos = line_out.text.Length;
3475 return;
3478 tag = tag.next;
3483 line_out = GetLine(lines);
3484 tag = line_out.tags;
3485 while (tag.next != null) {
3486 tag = tag.next;
3488 tag_out = tag;
3489 pos = line_out.text.Length;
3492 internal int LineTagToCharIndex(Line line, int pos) {
3493 int i;
3494 int length;
3496 // Count first and last line
3497 length = 0;
3499 // Count the lines in the middle
3501 for (i = 1; i < line.line_no; i++) {
3502 length += GetLine(i).text.Length + crlf_size;
3505 length += pos;
3507 return length;
3510 internal int SelectionLength() {
3511 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
3512 return 0;
3515 if (!multiline || (selection_start.line == selection_end.line)) {
3516 return selection_end.pos - selection_start.pos;
3517 } else {
3518 int i;
3519 int start;
3520 int end;
3521 int length;
3523 // Count first and last line
3524 length = selection_start.line.text.Length - selection_start.pos + selection_end.pos + crlf_size;
3526 // Count the lines in the middle
3527 start = selection_start.line.line_no + 1;
3528 end = selection_end.line.line_no;
3530 if (start < end) {
3531 for (i = start; i < end; i++) {
3532 length += GetLine(i).text.Length + crlf_size;
3536 return length;
3543 /// <summary>Give it a Line number and it returns the Line object at with that line number</summary>
3544 internal Line GetLine(int LineNo) {
3545 Line line = document;
3547 while (line != sentinel) {
3548 if (LineNo == line.line_no) {
3549 return line;
3550 } else if (LineNo < line.line_no) {
3551 line = line.left;
3552 } else {
3553 line = line.right;
3557 return null;
3560 /// <summary>Retrieve the previous tag; walks line boundaries</summary>
3561 internal LineTag PreviousTag(LineTag tag) {
3562 Line l;
3564 if (tag.previous != null) {
3565 return tag.previous;
3568 // Next line
3569 if (tag.line.line_no == 1) {
3570 return null;
3573 l = GetLine(tag.line.line_no - 1);
3574 if (l != null) {
3575 LineTag t;
3577 t = l.tags;
3578 while (t.next != null) {
3579 t = t.next;
3581 return t;
3584 return null;
3587 /// <summary>Retrieve the next tag; walks line boundaries</summary>
3588 internal LineTag NextTag(LineTag tag) {
3589 Line l;
3591 if (tag.next != null) {
3592 return tag.next;
3595 // Next line
3596 l = GetLine(tag.line.line_no + 1);
3597 if (l != null) {
3598 return l.tags;
3601 return null;
3604 internal Line ParagraphStart(Line line) {
3605 while (line.soft_break) {
3606 line = GetLine(line.line_no - 1);
3608 return line;
3611 internal Line ParagraphEnd(Line line) {
3612 Line l;
3614 while (line.soft_break) {
3615 l = GetLine(line.line_no + 1);
3616 if ((l == null) || (!l.soft_break)) {
3617 break;
3619 line = l;
3621 return line;
3624 /// <summary>Give it a Y pixel coordinate and it returns the Line covering that Y coordinate</summary>
3625 internal Line GetLineByPixel(int y, bool exact) {
3626 Line line = document;
3627 Line last = null;
3629 while (line != sentinel) {
3630 last = line;
3631 if ((y >= line.Y) && (y < (line.Y+line.height))) {
3632 return line;
3633 } else if (y < line.Y) {
3634 line = line.left;
3635 } else {
3636 line = line.right;
3640 if (exact) {
3641 return null;
3643 return last;
3646 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
3647 internal LineTag FindTag(int x, int y, out int index, bool exact) {
3648 Line line;
3649 LineTag tag;
3651 line = GetLineByPixel(y, exact);
3652 if (line == null) {
3653 index = 0;
3654 return null;
3656 tag = line.tags;
3658 // Alignment adjustment
3659 x += line.align_shift;
3661 while (true) {
3662 if (x >= tag.X && x < (tag.X+tag.width)) {
3663 int end;
3665 end = tag.start + tag.length - 1;
3667 for (int pos = tag.start; pos < end; pos++) {
3668 if (x < line.widths[pos]) {
3669 index = pos;
3670 return LineTag.GetFinalTag (tag);
3673 index=end;
3674 return LineTag.GetFinalTag (tag);
3676 if (tag.next != null) {
3677 tag = tag.next;
3678 } else {
3679 if (exact) {
3680 index = 0;
3681 return null;
3684 index = line.text.Length;
3685 return LineTag.GetFinalTag (tag);
3690 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
3691 internal LineTag FindCursor(int x, int y, out int index) {
3692 Line line;
3693 LineTag tag;
3695 line = GetLineByPixel(y, false);
3696 tag = line.tags;
3698 // Adjust for alignment
3699 x -= line.align_shift;
3701 while (true) {
3702 if (x >= tag.X && x < (tag.X+tag.width)) {
3703 int end;
3705 end = tag.start + tag.length - 1;
3707 for (int pos = tag.start-1; pos < end; pos++) {
3708 // When clicking on a character, we position the cursor to whatever edge
3709 // of the character the click was closer
3710 if (x < (line.widths[pos] + ((line.widths[pos+1]-line.widths[pos])/2))) {
3711 index = pos;
3712 return tag;
3715 index=end;
3716 return tag;
3718 if (tag.next != null) {
3719 tag = tag.next;
3720 } else {
3721 index = line.text.Length;
3722 return tag;
3727 /// <summary>Format area of document in specified font and color</summary>
3728 /// <param name="start_pos">1-based start position on start_line</param>
3729 /// <param name="end_pos">1-based end position on end_line </param>
3730 internal void FormatText(Line start_line, int start_pos, Line end_line, int end_pos, Font font, Brush color) {
3731 Line l;
3733 // First, format the first line
3734 if (start_line != end_line) {
3735 // First line
3736 LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, font, color);
3738 // Format last line
3739 LineTag.FormatText(end_line, 1, end_pos, font, color);
3741 // Now all the lines inbetween
3742 for (int i = start_line.line_no + 1; i < end_line.line_no; i++) {
3743 l = GetLine(i);
3744 LineTag.FormatText(l, 1, l.text.Length, font, color);
3746 } else {
3747 // Special case, single line
3748 LineTag.FormatText(start_line, start_pos, end_pos - start_pos, font, color);
3752 /// <summary>Re-format areas of the document in specified font and color</summary>
3753 /// <param name="start_pos">1-based start position on start_line</param>
3754 /// <param name="end_pos">1-based end position on end_line </param>
3755 /// <param name="font">Font specifying attributes</param>
3756 /// <param name="color">Color (or NULL) to apply</param>
3757 /// <param name="apply">Attributes from font and color to apply</param>
3758 internal void FormatText(Line start_line, int start_pos, Line end_line, int end_pos, FontDefinition attributes) {
3759 Line l;
3761 // First, format the first line
3762 if (start_line != end_line) {
3763 // First line
3764 LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, attributes);
3766 // Format last line
3767 LineTag.FormatText(end_line, 1, end_pos - 1, attributes);
3769 // Now all the lines inbetween
3770 for (int i = start_line.line_no + 1; i < end_line.line_no; i++) {
3771 l = GetLine(i);
3772 LineTag.FormatText(l, 1, l.text.Length, attributes);
3774 } else {
3775 // Special case, single line
3776 LineTag.FormatText(start_line, start_pos, end_pos - start_pos, attributes);
3780 internal void RecalculateAlignments() {
3781 Line line;
3782 int line_no;
3784 line_no = 1;
3786 while (line_no <= lines) {
3787 line = GetLine(line_no);
3789 if (line != null) {
3790 switch (line.alignment) {
3791 case HorizontalAlignment.Left:
3792 line.align_shift = 0;
3793 break;
3794 case HorizontalAlignment.Center:
3795 line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2;
3796 break;
3797 case HorizontalAlignment.Right:
3798 line.align_shift = viewport_width - (int)line.widths[line.text.Length];
3799 break;
3803 line_no++;
3805 return;
3808 /// <summary>Calculate formatting for the whole document</summary>
3809 internal bool RecalculateDocument(Graphics g) {
3810 return RecalculateDocument(g, 1, this.lines, false);
3813 /// <summary>Calculate formatting starting at a certain line</summary>
3814 internal bool RecalculateDocument(Graphics g, int start) {
3815 return RecalculateDocument(g, start, this.lines, false);
3818 /// <summary>Calculate formatting within two given line numbers</summary>
3819 internal bool RecalculateDocument(Graphics g, int start, int end) {
3820 return RecalculateDocument(g, start, end, false);
3823 /// <summary>With optimize on, returns true if line heights changed</summary>
3824 internal bool RecalculateDocument(Graphics g, int start, int end, bool optimize) {
3825 Line line;
3826 int line_no;
3827 int Y;
3828 int new_width;
3829 bool changed;
3830 int shift;
3832 if (no_recalc) {
3833 recalc_pending = true;
3834 recalc_start = start;
3835 recalc_end = end;
3836 recalc_optimize = optimize;
3837 return false;
3840 Y = GetLine(start).Y;
3841 line_no = start;
3842 new_width = 0;
3843 shift = this.lines;
3844 if (!optimize) {
3845 changed = true; // We always return true if we run non-optimized
3846 } else {
3847 changed = false;
3850 while (line_no <= (end + this.lines - shift)) {
3851 line = GetLine(line_no++);
3852 line.Y = Y;
3854 if (!calc_pass) {
3855 if (!optimize) {
3856 line.RecalculateLine(g, this);
3857 } else {
3858 if (line.recalc && line.RecalculateLine(g, this)) {
3859 changed = true;
3860 // If the height changed, all subsequent lines change
3861 end = this.lines;
3862 shift = this.lines;
3865 } else {
3866 if (!optimize) {
3867 line.RecalculatePasswordLine(g, this);
3868 } else {
3869 if (line.recalc && line.RecalculatePasswordLine(g, this)) {
3870 changed = true;
3871 // If the height changed, all subsequent lines change
3872 end = this.lines;
3873 shift = this.lines;
3878 if (line.widths[line.text.Length] > new_width) {
3879 new_width = (int)line.widths[line.text.Length];
3882 // Calculate alignment
3883 if (line.alignment != HorizontalAlignment.Left) {
3884 if (line.alignment == HorizontalAlignment.Center) {
3885 line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2;
3886 } else {
3887 line.align_shift = viewport_width - (int)line.widths[line.text.Length] - 1;
3891 if (line_no > lines) {
3892 break;
3896 if (document_x != new_width) {
3897 document_x = new_width;
3898 if (WidthChanged != null) {
3899 WidthChanged(this, null);
3903 RecalculateAlignments();
3905 line = GetLine(lines);
3907 if (document_y != line.Y + line.height) {
3908 document_y = line.Y + line.height;
3909 if (HeightChanged != null) {
3910 HeightChanged(this, null);
3913 UpdateCaret();
3914 return changed;
3917 internal int Size() {
3918 return lines;
3921 private void owner_HandleCreated(object sender, EventArgs e) {
3922 RecalculateDocument(owner.CreateGraphicsInternal());
3923 AlignCaret();
3926 private void owner_VisibleChanged(object sender, EventArgs e) {
3927 if (owner.Visible) {
3928 RecalculateDocument(owner.CreateGraphicsInternal());
3932 internal static bool IsWordSeparator(char ch) {
3933 switch(ch) {
3934 case ' ':
3935 case '\t':
3936 case '(':
3937 case ')': {
3938 return true;
3941 default: {
3942 return false;
3946 internal int FindWordSeparator(Line line, int pos, bool forward) {
3947 int len;
3949 len = line.text.Length;
3951 if (forward) {
3952 for (int i = pos + 1; i < len; i++) {
3953 if (IsWordSeparator(line.Text[i])) {
3954 return i + 1;
3957 return len;
3958 } else {
3959 for (int i = pos - 1; i > 0; i--) {
3960 if (IsWordSeparator(line.Text[i - 1])) {
3961 return i;
3964 return 0;
3968 /* Search document for text */
3969 internal bool FindChars(char[] chars, Marker start, Marker end, out Marker result) {
3970 Line line;
3971 int line_no;
3972 int pos;
3973 int line_len;
3975 // Search for occurence of any char in the chars array
3976 result = new Marker();
3978 line = start.line;
3979 line_no = start.line.line_no;
3980 pos = start.pos;
3981 while (line_no <= end.line.line_no) {
3982 line_len = line.text.Length;
3983 while (pos < line_len) {
3984 for (int i = 0; i < chars.Length; i++) {
3985 if (line.text[pos] == chars[i]) {
3986 // Special case
3987 if ((line.line_no == end.line.line_no) && (pos >= end.pos)) {
3988 return false;
3991 result.line = line;
3992 result.pos = pos;
3993 return true;
3996 pos++;
3999 pos = 0;
4000 line_no++;
4001 line = GetLine(line_no);
4004 return false;
4007 // This version does not build one big string for searching, instead it handles
4008 // line-boundaries, which is faster and less memory intensive
4009 // FIXME - Depending on culture stuff we might have to create a big string and use culturespecific
4010 // search stuff and change it to accept and return positions instead of Markers (which would match
4011 // RichTextBox behaviour better but would be inconsistent with the rest of TextControl)
4012 internal bool Find(string search, Marker start, Marker end, out Marker result, RichTextBoxFinds options) {
4013 Marker last;
4014 string search_string;
4015 Line line;
4016 int line_no;
4017 int pos;
4018 int line_len;
4019 int current;
4020 bool word;
4021 bool word_option;
4022 bool ignore_case;
4023 bool reverse;
4024 char c;
4026 result = new Marker();
4027 word_option = ((options & RichTextBoxFinds.WholeWord) != 0);
4028 ignore_case = ((options & RichTextBoxFinds.MatchCase) == 0);
4029 reverse = ((options & RichTextBoxFinds.Reverse) != 0);
4031 line = start.line;
4032 line_no = start.line.line_no;
4033 pos = start.pos;
4034 current = 0;
4036 // Prep our search string, lowercasing it if we do case-independent matching
4037 if (ignore_case) {
4038 StringBuilder sb;
4039 sb = new StringBuilder(search);
4040 for (int i = 0; i < sb.Length; i++) {
4041 sb[i] = Char.ToLower(sb[i]);
4043 search_string = sb.ToString();
4044 } else {
4045 search_string = search;
4048 // We need to check if the character before our start position is a wordbreak
4049 if (word_option) {
4050 if (line_no == 1) {
4051 if ((pos == 0) || (IsWordSeparator(line.text[pos - 1]))) {
4052 word = true;
4053 } else {
4054 word = false;
4056 } else {
4057 if (pos > 0) {
4058 if (IsWordSeparator(line.text[pos - 1])) {
4059 word = true;
4060 } else {
4061 word = false;
4063 } else {
4064 // Need to check the end of the previous line
4065 Line prev_line;
4067 prev_line = GetLine(line_no - 1);
4068 if (prev_line.soft_break) {
4069 if (IsWordSeparator(prev_line.text[prev_line.text.Length - 1])) {
4070 word = true;
4071 } else {
4072 word = false;
4074 } else {
4075 word = true;
4079 } else {
4080 word = false;
4083 // To avoid duplication of this loop with reverse logic, we search
4084 // through the document, remembering the last match and when returning
4085 // report that last remembered match
4087 last = new Marker();
4088 last.height = -1; // Abused - we use it to track change
4090 while (line_no <= end.line.line_no) {
4091 if (line_no != end.line.line_no) {
4092 line_len = line.text.Length;
4093 } else {
4094 line_len = end.pos;
4097 while (pos < line_len) {
4098 if (word_option && (current == search_string.Length)) {
4099 if (IsWordSeparator(line.text[pos])) {
4100 if (!reverse) {
4101 goto FindFound;
4102 } else {
4103 last = result;
4104 current = 0;
4106 } else {
4107 current = 0;
4111 if (ignore_case) {
4112 c = Char.ToLower(line.text[pos]);
4113 } else {
4114 c = line.text[pos];
4117 if (c == search_string[current]) {
4118 if (current == 0) {
4119 result.line = line;
4120 result.pos = pos;
4122 if (!word_option || (word_option && (word || (current > 0)))) {
4123 current++;
4126 if (!word_option && (current == search_string.Length)) {
4127 if (!reverse) {
4128 goto FindFound;
4129 } else {
4130 last = result;
4131 current = 0;
4134 } else {
4135 current = 0;
4137 pos++;
4139 if (!word_option) {
4140 continue;
4143 if (IsWordSeparator(c)) {
4144 word = true;
4145 } else {
4146 word = false;
4150 if (word_option) {
4151 // Mark that we just saw a word boundary
4152 if (!line.soft_break) {
4153 word = true;
4156 if (current == search_string.Length) {
4157 if (word) {
4158 if (!reverse) {
4159 goto FindFound;
4160 } else {
4161 last = result;
4162 current = 0;
4164 } else {
4165 current = 0;
4170 pos = 0;
4171 line_no++;
4172 line = GetLine(line_no);
4175 if (reverse) {
4176 if (last.height != -1) {
4177 result = last;
4178 return true;
4182 return false;
4184 FindFound:
4185 if (!reverse) {
4186 // if ((line.line_no == end.line.line_no) && (pos >= end.pos)) {
4187 // return false;
4188 // }
4189 return true;
4192 result = last;
4193 return true;
4197 /* Marker stuff */
4198 internal void GetMarker(out Marker mark, bool start) {
4199 mark = new Marker();
4201 if (start) {
4202 mark.line = GetLine(1);
4203 mark.tag = mark.line.tags;
4204 mark.pos = 0;
4205 } else {
4206 mark.line = GetLine(lines);
4207 mark.tag = mark.line.tags;
4208 while (mark.tag.next != null) {
4209 mark.tag = mark.tag.next;
4211 mark.pos = mark.line.text.Length;
4214 #endregion // Internal Methods
4216 #region Events
4217 internal event EventHandler CaretMoved;
4218 internal event EventHandler WidthChanged;
4219 internal event EventHandler HeightChanged;
4220 internal event EventHandler LengthChanged;
4221 #endregion // Events
4223 #region Administrative
4224 public IEnumerator GetEnumerator() {
4225 // FIXME
4226 return null;
4229 public override bool Equals(object obj) {
4230 if (obj == null) {
4231 return false;
4234 if (!(obj is Document)) {
4235 return false;
4238 if (obj == this) {
4239 return true;
4242 if (ToString().Equals(((Document)obj).ToString())) {
4243 return true;
4246 return false;
4249 public override int GetHashCode() {
4250 return document_id;
4253 public override string ToString() {
4254 return "document " + this.document_id;
4256 #endregion // Administrative
4259 internal class LineTag {
4260 #region Local Variables;
4261 // Payload; formatting
4262 internal Font font; // System.Drawing.Font object for this tag
4263 internal Brush color; // System.Drawing.Brush object
4265 // Payload; text
4266 internal int start; // start, in chars; index into Line.text
4267 internal int length; // length, in chars
4268 internal bool r_to_l; // Which way is the font
4270 // Drawing support
4271 internal int height; // Height in pixels of the text this tag describes
4272 internal int X; // X location of the text this tag describes
4273 internal float width; // Width in pixels of the text this tag describes
4274 internal int ascent; // Ascent of the font for this tag
4275 internal int shift; // Shift down for this tag, to stay on baseline
4277 // Administrative
4278 internal Line line; // The line we're on
4279 internal LineTag next; // Next tag on the same line
4280 internal LineTag previous; // Previous tag on the same line
4281 #endregion;
4283 #region Constructors
4284 internal LineTag(Line line, int start, int length) {
4285 this.line = line;
4286 this.start = start;
4287 this.length = length;
4288 this.X = 0;
4289 this.width = 0;
4291 #endregion // Constructors
4293 #region Internal Methods
4294 ///<summary>Break a tag into two with identical attributes; pos is 1-based; returns tag starting at &gt;pos&lt; or null if end-of-line</summary>
4295 internal LineTag Break(int pos) {
4296 LineTag new_tag;
4298 // Sanity
4299 if (pos == this.start) {
4300 return this;
4301 } else if (pos >= (start + length)) {
4302 return null;
4305 new_tag = new LineTag(line, pos, start + length - pos);
4306 new_tag.color = color;
4307 new_tag.font = font;
4308 this.length -= new_tag.length;
4309 new_tag.next = this.next;
4310 this.next = new_tag;
4311 new_tag.previous = this;
4312 if (new_tag.next != null) {
4313 new_tag.next.previous = new_tag;
4316 return new_tag;
4319 ///<summary>Create new font and brush from existing font and given new attributes. Returns true if fontheight changes</summary>
4320 internal static bool GenerateTextFormat(Font font_from, Brush color_from, FontDefinition attributes, out Font new_font, out Brush new_color) {
4321 float size;
4322 string face;
4323 FontStyle style;
4324 GraphicsUnit unit;
4326 if (attributes.font_obj == null) {
4327 size = font_from.SizeInPoints;
4328 unit = font_from.Unit;
4329 face = font_from.Name;
4330 style = font_from.Style;
4332 if (attributes.face != null) {
4333 face = attributes.face;
4336 if (attributes.size != 0) {
4337 size = attributes.size;
4340 style |= attributes.add_style;
4341 style &= ~attributes.remove_style;
4343 // Create new font
4344 new_font = new Font(face, size, style, unit);
4345 } else {
4346 new_font = attributes.font_obj;
4349 // Create 'new' color brush
4350 if (attributes.color != Color.Empty) {
4351 new_color = new SolidBrush(attributes.color);
4352 } else {
4353 new_color = color_from;
4356 if (new_font.Height == font_from.Height) {
4357 return false;
4359 return true;
4362 /// <summary>Applies 'font' and 'brush' to characters starting at 'start' for 'length' chars;
4363 /// Removes any previous tags overlapping the same area;
4364 /// returns true if lineheight has changed</summary>
4365 /// <param name="start">1-based character position on line</param>
4366 internal static bool FormatText(Line line, int start, int length, Font font, Brush color) {
4367 LineTag tag;
4368 LineTag start_tag;
4369 LineTag end_tag;
4370 int end;
4371 bool retval = false; // Assume line-height doesn't change
4373 // Too simple?
4374 if (font.Height != line.height) {
4375 retval = true;
4377 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
4379 // A little sanity, not sure if it's needed, might be able to remove for speed
4380 if (length > line.text.Length) {
4381 length = line.text.Length;
4384 tag = line.tags;
4385 end = start + length;
4387 // Common special case
4388 if ((start == 1) && (length == tag.length)) {
4389 tag.ascent = 0;
4390 tag.font = font;
4391 tag.color = color;
4392 return retval;
4395 //Console.WriteLine("Finding tag for {0} {1}", line, start);
4396 start_tag = FindTag(line, start);
4397 end_tag = FindTag (line, end);
4399 if (start_tag == null) { // FIXME - is there a better way to handle this, or do we even need it?
4400 throw new Exception(String.Format("Could not find start_tag in document at line {0} position {1}", line.line_no, start));
4403 tag = new LineTag(line, start, length);
4404 tag.font = font;
4405 tag.color = color;
4407 if (start == 1) {
4408 line.tags = tag;
4410 //Console.WriteLine("Start tag: '{0}'", start_tag!=null ? start_tag.ToString() : "NULL");
4411 if (start_tag.start == start) {
4412 tag.next = start_tag;
4413 tag.previous = start_tag.previous;
4414 if (start_tag.previous != null) {
4415 start_tag.previous.next = tag;
4417 start_tag.previous = tag;
4418 } else {
4419 tag.next = end_tag;
4421 if (end_tag != null) {
4422 // Shorten up the end tag
4423 end_tag.previous = tag;
4424 end_tag.length = end - start_tag.start + start_tag.length;
4425 end_tag.start = end;
4429 // Elimination loop
4430 tag = tag.next;
4431 while (tag != end_tag) {
4432 if ((tag.start + tag.length) <= end) {
4433 // remove the tag
4434 tag.previous.next = tag.next;
4435 if (tag.next != null) {
4436 tag.next.previous = tag.previous;
4438 tag = tag.previous;
4440 tag = tag.next;
4443 return retval;
4446 /// <summary>Applies font attributes specified to characters starting at 'start' for 'length' chars;
4447 /// Breaks tags at start and end point, keeping middle tags with altered attributes.
4448 /// Returns true if lineheight has changed</summary>
4449 /// <param name="start">1-based character position on line</param>
4450 internal static bool FormatText(Line line, int start, int length, FontDefinition attributes) {
4451 LineTag tag;
4452 LineTag start_tag;
4453 LineTag end_tag;
4454 bool retval = false; // Assume line-height doesn't change
4456 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
4458 // A little sanity, not sure if it's needed, might be able to remove for speed
4459 if (length > line.text.Length) {
4460 length = line.text.Length;
4463 tag = line.tags;
4465 // Common special case
4466 if ((start == 1) && (length == tag.length)) {
4467 tag.ascent = 0;
4468 GenerateTextFormat(tag.font, tag.color, attributes, out tag.font, out tag.color);
4469 return retval;
4472 start_tag = FindTag(line, start);
4474 if (start_tag == null) {
4475 if (length == 0) {
4476 // We are 'starting' after all valid tags; create a new tag with the right attributes
4477 start_tag = FindTag(line, line.text.Length - 1);
4478 start_tag.next = new LineTag(line, line.text.Length + 1, 0);
4479 start_tag.next.font = start_tag.font;
4480 start_tag.next.color = start_tag.color;
4481 start_tag.next.previous = start_tag;
4482 start_tag = start_tag.next;
4483 } else {
4484 throw new Exception(String.Format("Could not find start_tag in document at line {0} position {1}", line.line_no, start));
4486 } else {
4487 start_tag = start_tag.Break(start);
4490 end_tag = FindTag(line, start + length);
4491 if (end_tag != null) {
4492 end_tag = end_tag.Break(start + length);
4495 // start_tag or end_tag might be null; we're cool with that
4496 // we now walk from start_tag to end_tag, applying new attributes
4497 tag = start_tag;
4498 while ((tag != null) && tag != end_tag) {
4499 if (LineTag.GenerateTextFormat(tag.font, tag.color, attributes, out tag.font, out tag.color)) {
4500 retval = true;
4502 tag = tag.next;
4504 return retval;
4508 /// <summary>Finds the tag that describes the character at position 'pos' on 'line'</summary>
4509 internal static LineTag FindTag(Line line, int pos) {
4510 LineTag tag = line.tags;
4512 // Beginning of line is a bit special
4513 if (pos == 0) {
4514 // Not sure if we should get the final tag here
4515 return tag;
4518 while (tag != null) {
4519 if ((tag.start <= pos) && (pos < (tag.start+tag.length))) {
4520 return GetFinalTag (tag);
4523 tag = tag.next;
4526 return null;
4529 // There can be multiple tags at the same position, we want to make
4530 // sure we are using the very last tag at the given position
4531 internal static LineTag GetFinalTag (LineTag tag)
4533 LineTag res = tag;
4535 while (res.next != null && res.next.length == 0)
4536 res = res.next;
4537 return res;
4540 /// <summary>Combines 'this' tag with 'other' tag</summary>
4541 internal bool Combine(LineTag other) {
4542 if (!this.Equals(other)) {
4543 return false;
4546 this.width += other.width;
4547 this.length += other.length;
4548 this.next = other.next;
4549 if (this.next != null) {
4550 this.next.previous = this;
4553 return true;
4557 /// <summary>Remove 'this' tag ; to be called when formatting is to be removed</summary>
4558 internal bool Remove() {
4559 if ((this.start == 1) && (this.next == null)) {
4560 // We cannot remove the only tag
4561 return false;
4563 if (this.start != 1) {
4564 this.previous.length += this.length;
4565 this.previous.width = -1;
4566 this.previous.next = this.next;
4567 this.next.previous = this.previous;
4568 } else {
4569 this.next.start = 1;
4570 this.next.length += this.length;
4571 this.next.width = -1;
4572 this.line.tags = this.next;
4573 this.next.previous = null;
4575 return true;
4579 /// <summary>Checks if 'this' tag describes the same formatting options as 'obj'</summary>
4580 public override bool Equals(object obj) {
4581 LineTag other;
4583 if (obj == null) {
4584 return false;
4587 if (!(obj is LineTag)) {
4588 return false;
4591 if (obj == this) {
4592 return true;
4595 other = (LineTag)obj;
4597 if (this.font.Equals(other.font) && this.color.Equals(other.color)) { // FIXME add checking for things like link or type later
4598 return true;
4601 return false;
4604 public override int GetHashCode() {
4605 return base.GetHashCode ();
4608 public override string ToString() {
4609 if (length > 0)
4610 return "Tag starts at index " + this.start + "length " + this.length + " text: " + this.line.Text.Substring(this.start-1, this.length) + "Font " + this.font.ToString();
4611 return "Zero Lengthed tag at index " + this.start;
4614 #endregion // Internal Methods
4617 internal class UndoClass {
4618 internal enum ActionType {
4619 InsertChar,
4620 InsertString,
4621 DeleteChar,
4622 DeleteChars,
4623 CursorMove,
4624 Mark,
4625 CompoundBegin,
4626 CompoundEnd,
4629 internal class Action {
4630 internal ActionType type;
4631 internal int line_no;
4632 internal int pos;
4633 internal object data;
4636 #region Local Variables
4637 private Document document;
4638 private Stack undo_actions;
4639 private Stack redo_actions;
4640 private int caret_line;
4641 private int caret_pos;
4642 #endregion // Local Variables
4644 #region Constructors
4645 internal UndoClass(Document doc) {
4646 document = doc;
4647 undo_actions = new Stack(50);
4648 redo_actions = new Stack(50);
4650 #endregion // Constructors
4652 #region Properties
4653 [MonoTODO("Change this to be configurable")]
4654 internal int UndoLevels {
4655 get {
4656 return undo_actions.Count;
4660 [MonoTODO("Change this to be configurable")]
4661 internal int RedoLevels {
4662 get {
4663 return redo_actions.Count;
4667 [MonoTODO("Come up with good naming and localization")]
4668 internal string UndoName {
4669 get {
4670 Action action;
4672 action = (Action)undo_actions.Peek();
4673 switch(action.type) {
4674 case ActionType.InsertChar: {
4675 Locale.GetText("Insert character");
4676 break;
4679 case ActionType.DeleteChar: {
4680 Locale.GetText("Delete character");
4681 break;
4684 case ActionType.InsertString: {
4685 Locale.GetText("Insert string");
4686 break;
4689 case ActionType.DeleteChars: {
4690 Locale.GetText("Delete string");
4691 break;
4694 case ActionType.CursorMove: {
4695 Locale.GetText("Cursor move");
4696 break;
4699 return null;
4703 internal string RedoName() {
4704 return null;
4706 #endregion // Properties
4708 #region Internal Methods
4709 internal void Clear() {
4710 undo_actions.Clear();
4711 redo_actions.Clear();
4714 internal void Undo() {
4715 Action action;
4716 int compound_stack = 0;
4718 if (undo_actions.Count == 0) {
4719 return;
4724 do {
4725 action = (Action)undo_actions.Pop();
4727 // Put onto redo stack
4728 redo_actions.Push(action);
4730 // Do the thing
4731 switch(action.type) {
4732 case ActionType.CompoundEnd:
4733 compound_stack++;
4734 break;
4736 case ActionType.CompoundBegin:
4737 compound_stack--;
4738 break;
4740 case ActionType.InsertString:
4741 document.DeleteMultiline (document.GetLine (action.line_no),
4742 action.pos, ((string) action.data).Length + 1);
4743 break;
4745 case ActionType.InsertChar: {
4746 // FIXME - implement me
4747 break;
4750 case ActionType.DeleteChars: {
4751 this.Insert(document.GetLine(action.line_no), action.pos, (Line)action.data);
4752 Undo(); // Grab the cursor location
4753 break;
4756 case ActionType.CursorMove: {
4757 document.caret.line = document.GetLine(action.line_no);
4758 if (document.caret.line == null) {
4759 Undo();
4760 break;
4763 document.caret.tag = document.caret.line.FindTag(action.pos);
4764 document.caret.pos = action.pos;
4765 document.caret.height = document.caret.tag.height;
4767 if (document.owner.IsHandleCreated) {
4768 XplatUI.DestroyCaret(document.owner.Handle);
4769 XplatUI.CreateCaret(document.owner.Handle, 2, document.caret.height);
4770 XplatUI.SetCaretPos(document.owner.Handle, (int)document.caret.tag.line.widths[document.caret.pos] + document.caret.line.align_shift - document.viewport_x, document.caret.line.Y + document.caret.tag.shift - document.viewport_y + Document.caret_shift);
4772 document.DisplayCaret ();
4775 // FIXME - enable call
4776 //if (document.CaretMoved != null) document.CaretMoved(this, EventArgs.Empty);
4777 break;
4780 } while (compound_stack > 0);
4783 internal void Redo() {
4784 if (redo_actions.Count == 0) {
4785 return;
4788 #endregion // Internal Methods
4790 #region Private Methods
4792 public void BeginCompoundAction ()
4794 Action cb = new Action ();
4795 cb.type = ActionType.CompoundBegin;
4797 undo_actions.Push (cb);
4800 public void EndCompoundAction ()
4802 Action ce = new Action ();
4803 ce.type = ActionType.CompoundEnd;
4805 undo_actions.Push (ce);
4808 // pos = 1-based
4809 public void RecordDeleteChars(Line line, int pos, int length) {
4810 RecordDelete(line, pos, line, pos + length - 1);
4813 // start_pos, end_pos = 1 based
4814 public void RecordDelete(Line start_line, int start_pos, Line end_line, int end_pos) {
4815 Line l;
4816 Action a;
4818 l = Duplicate(start_line, start_pos, end_line, end_pos);
4820 a = new Action();
4821 a.type = ActionType.DeleteChars;
4822 a.data = l;
4823 a.line_no = start_line.line_no;
4824 a.pos = start_pos - 1;
4826 // Record the cursor position before, since the actions will occur in reverse order
4827 RecordCursor();
4828 undo_actions.Push(a);
4831 public void RecordInsertString (Line line, int pos, string str)
4833 Action a = new Action ();
4835 a.type = ActionType.InsertString;
4836 a.data = str;
4837 a.line_no = line.line_no;
4838 a.pos = pos;
4840 undo_actions.Push (a);
4843 public void RecordCursor() {
4844 if (document.caret.line == null) {
4845 return;
4848 RecordCursor(document.caret.line, document.caret.pos);
4851 public void RecordCursor(Line line, int pos) {
4852 Action a;
4854 if ((line.line_no == caret_line) && (pos == caret_pos)) {
4855 return;
4858 caret_line = line.line_no;
4859 caret_pos = pos;
4861 a = new Action();
4862 a.type = ActionType.CursorMove;
4863 a.line_no = line.line_no;
4864 a.pos = pos;
4866 undo_actions.Push(a);
4869 // start_pos = 1-based
4870 // end_pos = 1-based
4871 public Line Duplicate(Line start_line, int start_pos, Line end_line, int end_pos) {
4872 Line ret;
4873 Line line;
4874 Line current;
4875 LineTag tag;
4876 LineTag current_tag;
4877 int start;
4878 int end;
4879 int tag_start;
4880 int tag_length;
4882 line = new Line();
4883 ret = line;
4885 for (int i = start_line.line_no; i <= end_line.line_no; i++) {
4886 current = document.GetLine(i);
4888 if (start_line.line_no == i) {
4889 start = start_pos;
4890 } else {
4891 start = 1;
4894 if (end_line.line_no == i) {
4895 end = end_pos;
4896 } else {
4897 end = current.text.Length;
4900 // Text for the tag
4901 line.text = new StringBuilder(current.text.ToString(start - 1, end - start + 1));
4903 // Copy tags from start to start+length onto new line
4904 current_tag = current.FindTag(start - 1);
4905 while ((current_tag != null) && (current_tag.start < end)) {
4906 if ((current_tag.start <= start) && (start < (current_tag.start + current_tag.length))) {
4907 // start tag is within this tag
4908 tag_start = start;
4909 } else {
4910 tag_start = current_tag.start;
4913 if (end < (current_tag.start + current_tag.length)) {
4914 tag_length = end - tag_start + 1;
4915 } else {
4916 tag_length = current_tag.start + current_tag.length - tag_start;
4918 tag = new LineTag(line, tag_start - start + 1, tag_length);
4919 tag.color = current_tag.color;
4920 tag.font = current_tag.font;
4922 current_tag = current_tag.next;
4924 // Add the new tag to the line
4925 if (line.tags == null) {
4926 line.tags = tag;
4927 } else {
4928 LineTag tail;
4929 tail = line.tags;
4931 while (tail.next != null) {
4932 tail = tail.next;
4934 tail.next = tag;
4935 tag.previous = tail;
4939 if ((i + 1) <= end_line.line_no) {
4940 line.soft_break = current.soft_break;
4942 // Chain them (we use right/left as next/previous)
4943 line.right = new Line();
4944 line.right.left = line;
4945 line = line.right;
4949 return ret;
4952 // Insert multi-line text at the given position; use formatting at insertion point for inserted text
4953 internal void Insert(Line line, int pos, Line insert) {
4954 Line current;
4955 LineTag tag;
4956 int offset;
4957 int lines;
4958 Line first;
4960 // Handle special case first
4961 if (insert.right == null) {
4963 // Single line insert
4964 document.Split(line, pos);
4966 if (insert.tags == null) {
4967 return; // Blank line
4970 //Insert our tags at the end
4971 tag = line.tags;
4973 while (tag.next != null) {
4974 tag = tag.next;
4977 offset = tag.start + tag.length - 1;
4979 tag.next = insert.tags;
4980 line.text.Insert(offset, insert.text.ToString());
4982 // Adjust start locations
4983 tag = tag.next;
4984 while (tag != null) {
4985 tag.start += offset;
4986 tag.line = line;
4987 tag = tag.next;
4989 // Put it back together
4990 document.Combine(line.line_no, line.line_no + 1);
4991 document.UpdateView(line, pos);
4992 return;
4995 first = line;
4996 lines = 1;
4997 current = insert;
4998 while (current != null) {
4999 if (current == insert) {
5000 // Inserting the first line we split the line (and make space)
5001 document.Split(line, pos);
5002 //Insert our tags at the end of the line
5003 tag = line.tags;
5005 if (tag != null) {
5006 while (tag.next != null) {
5007 tag = tag.next;
5009 offset = tag.start + tag.length - 1;
5010 tag.next = current.tags;
5011 tag.next.previous = tag;
5013 tag = tag.next;
5015 } else {
5016 offset = 0;
5017 line.tags = current.tags;
5018 line.tags.previous = null;
5019 tag = line.tags;
5021 } else {
5022 document.Split(line.line_no, 0);
5023 offset = 0;
5024 line.tags = current.tags;
5025 line.tags.previous = null;
5026 tag = line.tags;
5028 // Adjust start locations and line pointers
5029 while (tag != null) {
5030 tag.start += offset;
5031 tag.line = line;
5032 tag = tag.next;
5035 line.text.Insert(offset, current.text.ToString());
5036 line.Grow(line.text.Length);
5038 line.recalc = true;
5039 line = document.GetLine(line.line_no + 1);
5041 // FIXME? Test undo of line-boundaries
5042 if ((current.right == null) && (current.tags.length != 0)) {
5043 document.Combine(line.line_no - 1, line.line_no);
5045 current = current.right;
5046 lines++;
5050 // Recalculate our document
5051 document.UpdateView(first, lines, pos);
5052 return;
5054 #endregion // Private Methods