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:
9 // The above copyright notice and this permission notice shall be
10 // included in all copies or substantial portions of the Software.
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)
23 // Peter Bartok pbartok@novell.com
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, etc.
34 // - Implement CaretPgUp/PgDown
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
49 using System.Collections;
51 using System.Drawing.Text;
54 namespace System.Windows.Forms {
55 internal enum LineColor {
60 internal enum CaretSelection {
61 Position, // Selection=Caret
62 Word, // Selection=Word under caret
63 Line // Selection=Line under caret
66 internal class FontDefinition {
69 internal FontStyle add_style;
70 internal FontStyle remove_style;
72 internal Font font_obj;
76 internal enum FormatSpecified {
84 internal enum CaretDirection {
85 CharForward, // Move a char to the right
86 CharBack, // Move a char to the left
87 LineUp, // Move a line up
88 LineDown, // Move a line down
89 Home, // Move to the beginning of the line
90 End, // Move to the end of the line
91 PgUp, // Move one page up
92 PgDn, // Move one page down
93 CtrlPgUp, // Move caret to the first visible char in the viewport
94 CtrlPgDn, // Move caret to the last visible char in the viewport
95 CtrlHome, // Move to the beginning of the document
96 CtrlEnd, // Move to the end of the document
97 WordBack, // Move to the beginning of the previous word (or beginning of line)
98 WordForward, // Move to the beginning of the next word (or end of line)
99 SelectionStart, // Move to the beginning of the current selection
100 SelectionEnd, // Move to the end of the current selection
101 CharForwardNoWrap, // Move a char forward, but don't wrap onto the next line
102 CharBackNoWrap // Move a char backward, but don't wrap onto the previous line
105 // Being cloneable should allow for nice line and document copies...
106 internal class Line : ICloneable, IComparable {
107 #region Local Variables
108 // Stuff that matters for our line
109 internal StringBuilder text; // Characters for the line
110 internal float[] widths; // Width of each character; always one larger than text.Length
111 internal int space; // Number of elements in text and widths
112 internal int line_no; // Line number
113 internal LineTag tags; // Tags describing the text
114 internal int Y; // Baseline
115 internal int height; // Height of the line (height of tallest tag)
116 internal int ascent; // Ascent of the line (ascent of the tallest tag)
117 internal HorizontalAlignment alignment; // Alignment of the line
118 internal int align_shift; // Pixel shift caused by the alignment
119 internal bool soft_break; // Tag is 'broken soft' and continuation from previous line
120 internal int indent; // Left indent for the first line
121 internal int hanging_indent; // Hanging indent (left indent for all but the first line)
122 internal int right_indent; // Right indent for all lines
123 internal bool carriage_return;
126 // Stuff that's important for the tree
127 internal Line parent; // Our parent line
128 internal Line left; // Line with smaller line number
129 internal Line right; // Line with higher line number
130 internal LineColor color; // We're doing a black/red tree. this is the node color
131 internal int DEFAULT_TEXT_LEN; //
132 internal bool recalc; // Line changed
133 #endregion // Local Variables
137 color = LineColor.Red;
144 alignment = HorizontalAlignment.Left;
147 internal Line(int LineNo, string Text, Font font, SolidBrush color) : this() {
148 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
150 text = new StringBuilder(Text, space);
153 widths = new float[space + 1];
154 tags = new LineTag(this, 1);
159 internal Line(int LineNo, string Text, HorizontalAlignment align, Font font, SolidBrush color) : this() {
160 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
162 text = new StringBuilder(Text, space);
166 widths = new float[space + 1];
167 tags = new LineTag(this, 1);
172 internal Line(int LineNo, string Text, LineTag tag) : this() {
173 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
175 text = new StringBuilder(Text, space);
178 widths = new float[space + 1];
182 #endregion // Constructors
184 #region Internal Properties
185 internal int Indent {
196 internal int HangingIndent {
198 return hanging_indent;
202 hanging_indent = value;
207 internal int RightIndent {
213 right_indent = value;
219 internal int Height {
229 internal int LineNo {
239 internal string Text {
241 return text.ToString();
245 text = new StringBuilder(value, value.Length > DEFAULT_TEXT_LEN ? value.Length : DEFAULT_TEXT_LEN);
249 internal HorizontalAlignment Alignment {
255 if (alignment != value) {
262 internal StringBuilder Text {
272 #endregion // Internal Properties
274 #region Internal Methods
275 // Make sure we always have enoughs space in text and widths
276 internal void Grow(int minimum) {
280 length = text.Length;
282 if ((length + minimum) > space) {
283 // We need to grow; double the size
285 if ((length + minimum) > (space * 2)) {
286 new_widths = new float[length + minimum * 2 + 1];
287 space = length + minimum * 2;
289 new_widths = new float[space * 2 + 1];
292 widths.CopyTo(new_widths, 0);
298 internal void Streamline(int lines) {
305 // Catch what the loop below wont; eliminate 0 length
306 // tags, but only if there are other tags after us
307 while ((current.length == 0) && (next != null)) {
309 tags.previous = null;
318 while (next != null) {
319 // Take out 0 length tags unless it's the last tag in the document
320 if (next.length == 0) {
321 if ((next.next != null) || (line_no != lines)) {
322 current.next = next.next;
323 if (current.next != null) {
324 current.next.previous = current;
330 if (current.Combine(next)) {
335 current = current.next;
340 /// <summary> Find the tag on a line based on the character position, pos is 0-based</summary>
341 internal LineTag FindTag(int pos) {
350 if (pos >= text.Length) {
351 pos = text.Length - 1;
354 while (tag != null) {
355 if (((tag.start - 1) <= pos) && (pos < (tag.start + tag.length - 1))) {
356 return LineTag.GetFinalTag (tag);
364 /// Recalculate a single line using the same char for every character in the line
367 internal bool RecalculatePasswordLine(Graphics g, Document doc) {
376 len = this.text.Length;
384 w = g.MeasureString(doc.password_char, tags.font, 10000, Document.string_format).Width;
386 if (this.height != (int)tag.font.Height) {
392 this.height = (int)tag.font.Height;
393 tag.height = this.height;
395 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
396 this.ascent = tag.ascent;
400 widths[pos] = widths[pos-1] + w;
407 /// Go through all tags on a line and recalculate all size-related values;
408 /// returns true if lineheight changed
410 internal bool RecalculateLine(Graphics g, Document doc) {
423 len = this.text.Length;
425 prev_height = this.height; // For drawing optimization calculations
426 this.height = 0; // Reset line height
427 this.ascent = 0; // Reset the ascent for the line
430 if (this.soft_break) {
431 widths[0] = hanging_indent;
444 while (tag.length == 0) { // We should always have tags after a tag.length==0 unless len==0
450 size = tag.SizeOfPosition (g, pos);
453 if (Char.IsWhiteSpace(text[pos])) {
458 if ((wrap_pos > 0) && (wrap_pos != len) && (widths[pos] + w) + 5 > (doc.viewport_width - this.right_indent)) {
459 // Make sure to set the last width of the line before wrapping
460 widths [pos + 1] = widths [pos] + w;
464 doc.Split(this, tag, pos, this.soft_break);
465 this.soft_break = true;
466 len = this.text.Length;
470 } else if (pos > 1 && (widths[pos] + w) > (doc.viewport_width - this.right_indent)) {
471 // No suitable wrap position was found so break right in the middle of a word
473 // Make sure to set the last width of the line before wrapping
474 widths [pos + 1] = widths [pos] + w;
476 doc.Split(this, tag, pos, this.soft_break);
477 this.soft_break = true;
478 len = this.text.Length;
484 // Contract all soft lines that follow back into our line
488 widths[pos] = widths[pos-1] + w;
491 line = doc.GetLine(this.line_no + 1);
492 if ((line != null) && soft_break) {
493 // Pull the two lines together
494 doc.Combine(this.line_no, this.line_no + 1);
495 len = this.text.Length;
501 if (pos == (tag.start-1 + tag.length)) {
502 // We just found the end of our current tag
503 tag.height = tag.MaxHeight ();
505 // Check if we're the tallest on the line (so far)
506 if (tag.height > this.height) {
507 this.height = tag.height; // Yep; make sure the line knows
510 if (tag.ascent == 0) {
513 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
516 if (tag.ascent > this.ascent) {
519 // We have a tag that has a taller ascent than the line;
521 while (t != null && t != tag) {
522 t.shift = tag.ascent - t.ascent;
527 this.ascent = tag.ascent;
529 tag.shift = this.ascent - tag.ascent;
540 if (this.height == 0) {
541 this.height = tags.font.Height;
542 tag.height = this.height;
545 if (prev_height != this.height) {
550 #endregion // Internal Methods
552 #region Administrative
553 public int CompareTo(object obj) {
558 if (! (obj is Line)) {
559 throw new ArgumentException("Object is not of type Line", "obj");
562 if (line_no < ((Line)obj).line_no) {
564 } else if (line_no > ((Line)obj).line_no) {
571 public object Clone() {
579 clone.left = (Line)left.Clone();
583 clone.left = (Line)left.Clone();
589 internal object CloneLine() {
599 public override bool Equals(object obj) {
604 if (!(obj is Line)) {
612 if (line_no == ((Line)obj).line_no) {
619 public override int GetHashCode() {
620 return base.GetHashCode ();
623 public override string ToString() {
624 return "Line " + line_no;
627 #endregion // Administrative
630 internal class Document : ICloneable, IEnumerable {
632 // FIXME - go through code and check for places where
633 // we do explicit comparisons instead of using the compare overloads
634 internal struct Marker {
636 internal LineTag tag;
640 public static bool operator<(Marker lhs, Marker rhs) {
641 if (lhs.line.line_no < rhs.line.line_no) {
645 if (lhs.line.line_no == rhs.line.line_no) {
646 if (lhs.pos < rhs.pos) {
653 public static bool operator>(Marker lhs, Marker rhs) {
654 if (lhs.line.line_no > rhs.line.line_no) {
658 if (lhs.line.line_no == rhs.line.line_no) {
659 if (lhs.pos > rhs.pos) {
666 public static bool operator==(Marker lhs, Marker rhs) {
667 if ((lhs.line.line_no == rhs.line.line_no) && (lhs.pos == rhs.pos)) {
673 public static bool operator!=(Marker lhs, Marker rhs) {
674 if ((lhs.line.line_no != rhs.line.line_no) || (lhs.pos != rhs.pos)) {
680 public void Combine(Line move_to_line, int move_to_line_length) {
682 pos += move_to_line_length;
683 tag = LineTag.FindTag(line, pos);
686 // This is for future use, right now Document.Split does it by hand, with some added shortcut logic
687 public void Split(Line move_to_line, int split_at) {
690 tag = LineTag.FindTag(line, pos);
693 public override bool Equals(object obj) {
694 return this==(Marker)obj;
697 public override int GetHashCode() {
698 return base.GetHashCode ();
701 public override string ToString() {
702 return "Marker Line " + line + ", Position " + pos;
706 #endregion Structures
708 #region Local Variables
709 private Line document;
711 private Line sentinel;
712 private int document_id;
713 private Random random = new Random();
714 internal string password_char;
715 private StringBuilder password_cache;
716 private bool calc_pass;
717 private int char_count;
719 // For calculating widths/heights
720 public static readonly StringFormat string_format = new StringFormat (StringFormat.GenericTypographic);
722 private int recalc_suspended;
723 private bool recalc_pending;
724 private int recalc_start = 1; // This starts at one, since lines are 1 based
725 private int recalc_end;
726 private bool recalc_optimize;
728 private int update_suspended;
729 private bool update_pending;
730 private int update_start = 1;
732 internal bool multiline;
735 internal UndoManager undo;
737 internal Marker caret;
738 internal Marker selection_start;
739 internal Marker selection_end;
740 internal bool selection_visible;
741 internal Marker selection_anchor;
742 internal Marker selection_prev;
743 internal bool selection_end_anchor;
745 internal int viewport_x;
746 internal int viewport_y; // The visible area of the document
747 internal int viewport_width;
748 internal int viewport_height;
750 internal int document_x; // Width of the document
751 internal int document_y; // Height of the document
753 internal Rectangle invalid;
755 internal int crlf_size; // 1 or 2, depending on whether we use \r\n or just \n
757 internal TextBoxBase owner; // Who's owning us?
758 static internal int caret_width = 1;
759 static internal int caret_shift = 1;
760 #endregion // Local Variables
763 internal Document(TextBoxBase owner) {
771 recalc_pending = false;
773 // Tree related stuff
774 sentinel = new Line();
775 sentinel.color = LineColor.Black;
779 // We always have a blank line
780 owner.HandleCreated += new EventHandler(owner_HandleCreated);
781 owner.VisibleChanged += new EventHandler(owner_VisibleChanged);
783 Add(1, "", owner.Font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.ForeColor));
784 Line l = GetLine (1);
787 undo = new UndoManager (this);
789 selection_visible = false;
790 selection_start.line = this.document;
791 selection_start.pos = 0;
792 selection_start.tag = selection_start.line.tags;
793 selection_end.line = this.document;
794 selection_end.pos = 0;
795 selection_end.tag = selection_end.line.tags;
796 selection_anchor.line = this.document;
797 selection_anchor.pos = 0;
798 selection_anchor.tag = selection_anchor.line.tags;
799 caret.line = this.document;
801 caret.tag = caret.line.tags;
808 // Default selection is empty
810 document_id = random.Next();
812 string_format.Trimming = StringTrimming.None;
813 string_format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;
817 #region Internal Properties
834 internal Line CaretLine {
840 internal int CaretPosition {
846 internal Point Caret {
848 return new Point((int)caret.tag.line.widths[caret.pos] + caret.line.align_shift, caret.line.Y);
852 internal LineTag CaretTag {
862 internal int CRLFSize {
872 internal string PasswordChar {
874 return password_char;
878 password_char = value;
879 PasswordCache.Length = 0;
880 if ((password_char.Length != 0) && (password_char[0] != '\0')) {
888 private StringBuilder PasswordCache {
890 if (password_cache == null)
891 password_cache = new StringBuilder();
892 return password_cache;
896 internal int ViewPortX {
906 internal int Length {
908 return char_count + lines - 1; // Add \n for each line but the last
912 private int CharCount {
920 if (LengthChanged != null) {
921 LengthChanged(this, EventArgs.Empty);
926 internal int ViewPortY {
936 internal int ViewPortWidth {
938 return viewport_width;
942 viewport_width = value;
946 internal int ViewPortHeight {
948 return viewport_height;
952 viewport_height = value;
959 return this.document_x;
963 internal int Height {
965 return this.document_y;
969 internal bool SelectionVisible {
971 return selection_visible;
985 #endregion // Internal Properties
987 #region Private Methods
989 internal void SuspendRecalc ()
994 internal void ResumeRecalc (bool immediate_update)
996 if (recalc_suspended > 0)
999 if (immediate_update && recalc_suspended == 0 && recalc_pending) {
1000 RecalculateDocument (owner.CreateGraphicsInternal(), recalc_start, recalc_end, recalc_optimize);
1001 recalc_pending = false;
1005 internal void SuspendUpdate ()
1010 internal void ResumeUpdate (bool immediate_update)
1012 if (update_suspended > 0)
1015 if (immediate_update && update_suspended == 0 && update_pending) {
1016 UpdateView (GetLine (update_start), 0);
1017 update_pending = false;
1022 internal int DumpTree(Line line, bool with_tags) {
1027 Console.Write("Line {0} [# {1}], Y: {2}, soft: {3}, Text: '{4}'",
1028 line.line_no, line.GetHashCode(), line.Y, line.soft_break,
1029 line.text != null ? line.text.ToString() : "undefined");
1031 if (line.left == sentinel) {
1032 Console.Write(", left = sentinel");
1033 } else if (line.left == null) {
1034 Console.Write(", left = NULL");
1037 if (line.right == sentinel) {
1038 Console.Write(", right = sentinel");
1039 } else if (line.right == null) {
1040 Console.Write(", right = NULL");
1043 Console.WriteLine("");
1053 Console.Write(" Tags: ");
1054 while (tag != null) {
1055 Console.Write("{0} <{1}>-<{2}>", count++, tag.start, tag.end
1056 /*line.text.ToString (tag.start - 1, tag.length)*/);
1057 length += tag.length;
1059 if (tag.line != line) {
1060 Console.Write("BAD line link");
1061 throw new Exception("Bad line link in tree");
1065 Console.Write(", ");
1068 if (length > line.text.Length) {
1069 throw new Exception(String.Format("Length of tags more than length of text on line (expected {0} calculated {1})", line.text.Length, length));
1070 } else if (length < line.text.Length) {
1071 throw new Exception(String.Format("Length of tags less than length of text on line (expected {0} calculated {1})", line.text.Length, length));
1073 Console.WriteLine("");
1075 if (line.left != null) {
1076 if (line.left != sentinel) {
1077 total += DumpTree(line.left, with_tags);
1080 if (line != sentinel) {
1081 throw new Exception("Left should not be NULL");
1085 if (line.right != null) {
1086 if (line.right != sentinel) {
1087 total += DumpTree(line.right, with_tags);
1090 if (line != sentinel) {
1091 throw new Exception("Right should not be NULL");
1095 for (int i = 1; i <= this.lines; i++) {
1096 if (GetLine(i) == null) {
1097 throw new Exception(String.Format("Hole in line order, missing {0}", i));
1101 if (line == this.Root) {
1102 if (total < this.lines) {
1103 throw new Exception(String.Format("Not enough nodes in tree, found {0}, expected {1}", total, this.lines));
1104 } else if (total > this.lines) {
1105 throw new Exception(String.Format("Too many nodes in tree, found {0}, expected {1}", total, this.lines));
1112 private void SetSelectionVisible (bool value)
1114 selection_visible = value;
1116 // cursor and selection are enemies, we can't have both in the same room at the same time
1117 if (owner.IsHandleCreated && !owner.show_caret_w_selection)
1118 XplatUI.CaretVisible (owner.Handle, !selection_visible);
1121 private void DecrementLines(int line_no) {
1125 while (current <= lines) {
1126 GetLine(current).line_no--;
1132 private void IncrementLines(int line_no) {
1135 current = this.lines;
1136 while (current >= line_no) {
1137 GetLine(current).line_no++;
1143 private void RebalanceAfterAdd(Line line1) {
1146 while ((line1 != document) && (line1.parent.color == LineColor.Red)) {
1147 if (line1.parent == line1.parent.parent.left) {
1148 line2 = line1.parent.parent.right;
1150 if ((line2 != null) && (line2.color == LineColor.Red)) {
1151 line1.parent.color = LineColor.Black;
1152 line2.color = LineColor.Black;
1153 line1.parent.parent.color = LineColor.Red;
1154 line1 = line1.parent.parent;
1156 if (line1 == line1.parent.right) {
1157 line1 = line1.parent;
1161 line1.parent.color = LineColor.Black;
1162 line1.parent.parent.color = LineColor.Red;
1164 RotateRight(line1.parent.parent);
1167 line2 = line1.parent.parent.left;
1169 if ((line2 != null) && (line2.color == LineColor.Red)) {
1170 line1.parent.color = LineColor.Black;
1171 line2.color = LineColor.Black;
1172 line1.parent.parent.color = LineColor.Red;
1173 line1 = line1.parent.parent;
1175 if (line1 == line1.parent.left) {
1176 line1 = line1.parent;
1180 line1.parent.color = LineColor.Black;
1181 line1.parent.parent.color = LineColor.Red;
1182 RotateLeft(line1.parent.parent);
1186 document.color = LineColor.Black;
1189 private void RebalanceAfterDelete(Line line1) {
1192 while ((line1 != document) && (line1.color == LineColor.Black)) {
1193 if (line1 == line1.parent.left) {
1194 line2 = line1.parent.right;
1195 if (line2.color == LineColor.Red) {
1196 line2.color = LineColor.Black;
1197 line1.parent.color = LineColor.Red;
1198 RotateLeft(line1.parent);
1199 line2 = line1.parent.right;
1201 if ((line2.left.color == LineColor.Black) && (line2.right.color == LineColor.Black)) {
1202 line2.color = LineColor.Red;
1203 line1 = line1.parent;
1205 if (line2.right.color == LineColor.Black) {
1206 line2.left.color = LineColor.Black;
1207 line2.color = LineColor.Red;
1209 line2 = line1.parent.right;
1211 line2.color = line1.parent.color;
1212 line1.parent.color = LineColor.Black;
1213 line2.right.color = LineColor.Black;
1214 RotateLeft(line1.parent);
1218 line2 = line1.parent.left;
1219 if (line2.color == LineColor.Red) {
1220 line2.color = LineColor.Black;
1221 line1.parent.color = LineColor.Red;
1222 RotateRight(line1.parent);
1223 line2 = line1.parent.left;
1225 if ((line2.right.color == LineColor.Black) && (line2.left.color == LineColor.Black)) {
1226 line2.color = LineColor.Red;
1227 line1 = line1.parent;
1229 if (line2.left.color == LineColor.Black) {
1230 line2.right.color = LineColor.Black;
1231 line2.color = LineColor.Red;
1233 line2 = line1.parent.left;
1235 line2.color = line1.parent.color;
1236 line1.parent.color = LineColor.Black;
1237 line2.left.color = LineColor.Black;
1238 RotateRight(line1.parent);
1243 line1.color = LineColor.Black;
1246 private void RotateLeft(Line line1) {
1247 Line line2 = line1.right;
1249 line1.right = line2.left;
1251 if (line2.left != sentinel) {
1252 line2.left.parent = line1;
1255 if (line2 != sentinel) {
1256 line2.parent = line1.parent;
1259 if (line1.parent != null) {
1260 if (line1 == line1.parent.left) {
1261 line1.parent.left = line2;
1263 line1.parent.right = line2;
1270 if (line1 != sentinel) {
1271 line1.parent = line2;
1275 private void RotateRight(Line line1) {
1276 Line line2 = line1.left;
1278 line1.left = line2.right;
1280 if (line2.right != sentinel) {
1281 line2.right.parent = line1;
1284 if (line2 != sentinel) {
1285 line2.parent = line1.parent;
1288 if (line1.parent != null) {
1289 if (line1 == line1.parent.right) {
1290 line1.parent.right = line2;
1292 line1.parent.left = line2;
1298 line2.right = line1;
1299 if (line1 != sentinel) {
1300 line1.parent = line2;
1305 internal void UpdateView(Line line, int pos) {
1306 if (!owner.IsHandleCreated) {
1310 if (update_suspended > 0) {
1311 update_start = Math.Min (update_start, line.line_no);
1312 // update_end = Math.Max (update_end, line.line_no);
1313 // recalc_optimize = true;
1314 update_pending = true;
1318 // Optimize invalidation based on Line alignment
1319 if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no, true)) {
1320 // Lineheight changed, invalidate the rest of the document
1321 if ((line.Y - viewport_y) >=0 ) {
1322 // We formatted something that's in view, only draw parts of the screen
1323 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, owner.Height - line.Y - viewport_y));
1325 // The tag was above the visible area, draw everything
1329 switch(line.alignment) {
1330 case HorizontalAlignment.Left: {
1331 owner.Invalidate(new Rectangle((int)line.widths[pos] - viewport_x - 1, line.Y - viewport_y, viewport_width, line.height + 1));
1335 case HorizontalAlignment.Center: {
1336 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, line.height + 1));
1340 case HorizontalAlignment.Right: {
1341 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, (int)line.widths[pos + 1] - viewport_x + line.align_shift, line.height + 1));
1349 // Update display from line, down line_count lines; pos is unused, but required for the signature
1350 internal void UpdateView(Line line, int line_count, int pos) {
1351 if (!owner.IsHandleCreated) {
1355 if (recalc_suspended > 0) {
1356 recalc_start = Math.Min (recalc_start, line.line_no);
1357 recalc_end = Math.Max (recalc_end, line.line_no + line_count - 1);
1358 recalc_optimize = true;
1359 recalc_pending = true;
1363 if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no + line_count - 1, true)) {
1364 // Lineheight changed, invalidate the rest of the document
1365 if ((line.Y - viewport_y) >=0 ) {
1366 // We formatted something that's in view, only draw parts of the screen
1367 //blah Console.WriteLine("TextControl.cs(981) Invalidate called in UpdateView(line, line_count, pos)");
1368 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, owner.Height - line.Y - viewport_y));
1370 // The tag was above the visible area, draw everything
1371 //blah Console.WriteLine("TextControl.cs(985) Invalidate called in UpdateView(line, line_count, pos)");
1377 end_line = GetLine(line.line_no + line_count -1);
1378 if (end_line == null) {
1382 //blah Console.WriteLine("TextControl.cs(996) Invalidate called in UpdateView(line, line_count, pos)");
1383 owner.Invalidate(new Rectangle(0 - viewport_x, line.Y - viewport_y, (int)line.widths[line.text.Length], end_line.Y + end_line.height));
1386 #endregion // Private Methods
1388 #region Internal Methods
1389 // Clear the document and reset state
1390 internal void Empty() {
1392 document = sentinel;
1395 // We always have a blank line
1396 Add(1, "", owner.Font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.ForeColor));
1397 Line l = GetLine (1);
1398 l.soft_break = true;
1400 this.RecalculateDocument(owner.CreateGraphicsInternal());
1401 PositionCaret(0, 0);
1403 SetSelectionVisible (false);
1405 selection_start.line = this.document;
1406 selection_start.pos = 0;
1407 selection_start.tag = selection_start.line.tags;
1408 selection_end.line = this.document;
1409 selection_end.pos = 0;
1410 selection_end.tag = selection_end.line.tags;
1419 if (owner.IsHandleCreated)
1420 owner.Invalidate ();
1423 internal void PositionCaret(Line line, int pos) {
1424 caret.tag = line.FindTag(pos);
1428 if (owner.IsHandleCreated) {
1429 if (owner.Focused) {
1430 if (caret.height != caret.tag.height)
1431 XplatUI.CreateCaret (owner.Handle, caret_width, caret.height);
1432 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);
1435 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1438 // We set this at the end because we use the heights to determine whether or
1439 // not we need to recreate the caret
1440 caret.height = caret.tag.height;
1444 internal void PositionCaret(int x, int y) {
1445 if (!owner.IsHandleCreated) {
1449 caret.tag = FindCursor(x, y, out caret.pos);
1450 caret.line = caret.tag.line;
1451 caret.height = caret.tag.height;
1453 if (owner.Focused) {
1454 XplatUI.CreateCaret (owner.Handle, caret_width, caret.height);
1455 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);
1458 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1461 internal void CaretHasFocus() {
1462 if ((caret.tag != null) && owner.IsHandleCreated) {
1463 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1464 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);
1469 if (owner.IsHandleCreated && selection_visible) {
1470 InvalidateSelectionArea ();
1474 internal void CaretLostFocus() {
1475 if (!owner.IsHandleCreated) {
1478 XplatUI.DestroyCaret(owner.Handle);
1481 internal void AlignCaret() {
1482 if (!owner.IsHandleCreated) {
1486 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1487 caret.height = caret.tag.height;
1489 if (owner.Focused) {
1490 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1491 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);
1495 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1498 internal void UpdateCaret() {
1499 if (!owner.IsHandleCreated || caret.tag == null) {
1503 if (caret.tag.height != caret.height) {
1504 caret.height = caret.tag.height;
1505 if (owner.Focused) {
1506 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1510 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);
1514 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1517 internal void DisplayCaret() {
1518 if (!owner.IsHandleCreated) {
1522 if (owner.ShowSelection && (!selection_visible || owner.show_caret_w_selection)) {
1523 XplatUI.CaretVisible(owner.Handle, true);
1527 internal void HideCaret() {
1528 if (!owner.IsHandleCreated) {
1532 if (owner.Focused) {
1533 XplatUI.CaretVisible(owner.Handle, false);
1537 internal void MoveCaret(CaretDirection direction) {
1538 // FIXME should we use IsWordSeparator to detect whitespace, instead
1539 // of looking for actual spaces in the Word move cases?
1541 bool nowrap = false;
1543 case CaretDirection.CharForwardNoWrap:
1545 goto case CaretDirection.CharForward;
1546 case CaretDirection.CharForward: {
1548 if (caret.pos > caret.line.text.Length) {
1549 if (multiline && !nowrap) {
1550 // Go into next line
1551 if (caret.line.line_no < this.lines) {
1552 caret.line = GetLine(caret.line.line_no+1);
1554 caret.tag = caret.line.tags;
1559 // Single line; we stay where we are
1563 if ((caret.tag.start - 1 + caret.tag.length) < caret.pos) {
1564 caret.tag = caret.tag.next;
1571 case CaretDirection.CharBackNoWrap:
1573 goto case CaretDirection.CharBack;
1574 case CaretDirection.CharBack: {
1575 if (caret.pos > 0) {
1576 // caret.pos--; // folded into the if below
1577 if (--caret.pos > 0) {
1578 if (caret.tag.start > caret.pos) {
1579 caret.tag = caret.tag.previous;
1583 if (caret.line.line_no > 1 && !nowrap) {
1584 caret.line = GetLine(caret.line.line_no - 1);
1585 caret.pos = caret.line.text.Length;
1586 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1593 case CaretDirection.WordForward: {
1596 len = caret.line.text.Length;
1597 if (caret.pos < len) {
1598 while ((caret.pos < len) && (caret.line.text[caret.pos] != ' ')) {
1601 if (caret.pos < len) {
1602 // Skip any whitespace
1603 while ((caret.pos < len) && (caret.line.text[caret.pos] == ' ')) {
1607 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1609 if (caret.line.line_no < this.lines) {
1610 caret.line = GetLine(caret.line.line_no + 1);
1612 caret.tag = caret.line.tags;
1619 case CaretDirection.WordBack: {
1620 if (caret.pos > 0) {
1623 while ((caret.pos > 0) && (caret.line.text[caret.pos] == ' ')) {
1627 while ((caret.pos > 0) && (caret.line.text[caret.pos] != ' ')) {
1631 if (caret.line.text.ToString(caret.pos, 1) == " ") {
1632 if (caret.pos != 0) {
1635 caret.line = GetLine(caret.line.line_no - 1);
1636 caret.pos = caret.line.text.Length;
1639 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1641 if (caret.line.line_no > 1) {
1642 caret.line = GetLine(caret.line.line_no - 1);
1643 caret.pos = caret.line.text.Length;
1644 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1651 case CaretDirection.LineUp: {
1652 if (caret.line.line_no > 1) {
1655 pixel = (int)caret.line.widths[caret.pos];
1656 PositionCaret(pixel, GetLine(caret.line.line_no - 1).Y);
1663 case CaretDirection.LineDown: {
1664 if (caret.line.line_no < lines) {
1667 pixel = (int)caret.line.widths[caret.pos];
1668 PositionCaret(pixel, GetLine(caret.line.line_no + 1).Y);
1675 case CaretDirection.Home: {
1676 if (caret.pos > 0) {
1678 caret.tag = caret.line.tags;
1684 case CaretDirection.End: {
1685 if (caret.pos < caret.line.text.Length) {
1686 caret.pos = caret.line.text.Length;
1687 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1693 case CaretDirection.PgUp: {
1695 int new_y, y_offset;
1697 if (viewport_y == 0) {
1699 // This should probably be handled elsewhere
1700 if (!(owner is RichTextBox)) {
1701 // Page down doesn't do anything in a regular TextBox
1702 // if the bottom of the document
1703 // is already visible, the page and the caret stay still
1707 // We're just placing the caret at the end of the document, no scrolling needed
1708 owner.vscroll.Value = 0;
1709 Line line = GetLine (1);
1710 PositionCaret (line, 0);
1713 y_offset = caret.line.Y - viewport_y;
1714 new_y = caret.line.Y - viewport_height;
1716 owner.vscroll.Value = Math.Max (new_y, 0);
1717 PositionCaret ((int)caret.line.widths[caret.pos], y_offset + viewport_y);
1721 case CaretDirection.PgDn: {
1722 int new_y, y_offset;
1724 if ((viewport_y + viewport_height) > document_y) {
1726 // This should probably be handled elsewhere
1727 if (!(owner is RichTextBox)) {
1728 // Page up doesn't do anything in a regular TextBox
1729 // if the bottom of the document
1730 // is already visible, the page and the caret stay still
1734 // We're just placing the caret at the end of the document, no scrolling needed
1735 owner.vscroll.Value = owner.vscroll.Maximum - viewport_height + 1;
1736 Line line = GetLine (lines);
1737 PositionCaret (line, line.Text.Length);
1740 y_offset = caret.line.Y - viewport_y;
1741 new_y = caret.line.Y + viewport_height;
1743 owner.vscroll.Value = Math.Min (new_y, owner.vscroll.Maximum - viewport_height + 1);
1744 PositionCaret ((int)caret.line.widths[caret.pos], y_offset + viewport_y);
1749 case CaretDirection.CtrlPgUp: {
1750 PositionCaret(0, viewport_y);
1755 case CaretDirection.CtrlPgDn: {
1760 tag = FindTag(0, viewport_y + viewport_height, out index, false);
1761 if (tag.line.line_no > 1) {
1762 line = GetLine(tag.line.line_no - 1);
1766 PositionCaret(line, line.Text.Length);
1771 case CaretDirection.CtrlHome: {
1772 caret.line = GetLine(1);
1774 caret.tag = caret.line.tags;
1780 case CaretDirection.CtrlEnd: {
1781 caret.line = GetLine(lines);
1782 caret.pos = caret.line.text.Length;
1783 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1789 case CaretDirection.SelectionStart: {
1790 caret.line = selection_start.line;
1791 caret.pos = selection_start.pos;
1792 caret.tag = selection_start.tag;
1798 case CaretDirection.SelectionEnd: {
1799 caret.line = selection_end.line;
1800 caret.pos = selection_end.pos;
1801 caret.tag = selection_end.tag;
1809 internal void DumpDoc ()
1811 Console.WriteLine ("<doc>");
1812 for (int i = 1; i < lines; i++) {
1813 Line line = GetLine (i);
1814 Console.WriteLine ("<line no='{0}'>", line.line_no);
1816 LineTag tag = line.tags;
1817 while (tag != null) {
1818 Console.Write ("\t<tag color='{0}'>", tag.color.Color);
1819 Console.Write (tag.Text ());
1820 Console.WriteLine ("\t</tag>");
1823 Console.WriteLine ("</line>");
1825 Console.WriteLine ("</doc>");
1828 internal void Draw (Graphics g, Rectangle clip)
1830 Line line; // Current line being drawn
1831 LineTag tag; // Current tag being drawn
1832 int start; // First line to draw
1833 int end; // Last line to draw
1834 StringBuilder text; // String representing the current line
1837 Brush current_brush;
1838 Brush disabled_brush;
1842 // First, figure out from what line to what line we need to draw
1843 start = GetLineByPixel(clip.Top + viewport_y, false).line_no;
1844 end = GetLineByPixel(clip.Bottom + viewport_y, false).line_no;
1846 /// Make sure that we aren't drawing one more line then we need to
1847 line = GetLine (end - 1);
1848 if (line != null && clip.Bottom == line.Y + line.height + viewport_y)
1854 DateTime n = DateTime.Now;
1855 Console.WriteLine ("Started drawing: {0}s {1}ms", n.Second, n.Millisecond);
1856 Console.WriteLine ("CLIP: {0}", clip);
1857 Console.WriteLine ("S: {0}", GetLine (start).text);
1858 Console.WriteLine ("E: {0}", GetLine (end).text);
1861 disabled_brush = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorGrayText);
1862 hilight = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorHighlight);
1863 hilight_text = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorHighlightText);
1865 while (line_no <= end) {
1866 line = GetLine (line_no);
1867 float line_y = line.Y - viewport_y;
1873 if (PasswordCache.Length < line.text.Length)
1874 PasswordCache.Append(Char.Parse(password_char), line.text.Length - PasswordCache.Length);
1875 else if (PasswordCache.Length > line.text.Length)
1876 PasswordCache.Remove(line.text.Length, PasswordCache.Length - line.text.Length);
1877 text = PasswordCache;
1880 int line_selection_start = text.Length + 1;
1881 int line_selection_end = text.Length + 1;
1882 if (selection_visible && owner.ShowSelection &&
1883 (line_no >= selection_start.line.line_no) &&
1884 (line_no <= selection_end.line.line_no)) {
1886 if (line_no == selection_start.line.line_no)
1887 line_selection_start = selection_start.pos + 1;
1889 line_selection_start = 1;
1891 if (line_no == selection_end.line.line_no)
1892 line_selection_end = selection_end.pos + 1;
1894 line_selection_end = text.Length + 1;
1896 if (line_selection_end == line_selection_start) {
1897 // There isn't really selection
1898 line_selection_start = text.Length + 1;
1899 line_selection_end = line_selection_start;
1901 // lets draw some selection baby!!
1903 g.FillRectangle (hilight,
1904 line.widths [line_selection_start - 1] + line.align_shift - viewport_x,
1905 line_y, line.widths [line_selection_end - 1] - line.widths [line_selection_start - 1],
1910 current_brush = line.tags.color;
1911 while (tag != null) {
1914 if (tag.length == 0) {
1919 if (((tag.X + tag.width) < (clip.Left - viewport_x)) && (tag.X > (clip.Right - viewport_x))) {
1924 if (tag.back_color != null) {
1925 g.FillRectangle (tag.back_color, tag.X + line.align_shift - viewport_x,
1926 line_y + tag.shift, tag.width, line.height);
1929 tag_brush = tag.color;
1930 current_brush = tag_brush;
1932 if (!owner.is_enabled) {
1933 Color a = ((SolidBrush) tag.color).Color;
1934 Color b = ThemeEngine.Current.ColorWindowText;
1936 if ((a.R == b.R) && (a.G == b.G) && (a.B == b.B)) {
1937 tag_brush = disabled_brush;
1941 int tag_pos = tag.start;
1942 current_brush = tag_brush;
1943 while (tag_pos < tag.start + tag.length) {
1944 int old_tag_pos = tag_pos;
1946 if (tag_pos >= line_selection_start && tag_pos < line_selection_end) {
1947 current_brush = hilight_text;
1948 tag_pos = Math.Min (tag.end, line_selection_end);
1949 } else if (tag_pos < line_selection_start) {
1950 current_brush = tag.color;
1951 tag_pos = Math.Min (tag.end, line_selection_start);
1953 current_brush = tag.color;
1957 tag.Draw (g, current_brush,
1958 line.widths [old_tag_pos - 1] + line.align_shift - viewport_x,
1960 old_tag_pos - 1, Math.Min (tag.length, tag_pos - old_tag_pos),
1969 private void InsertLineString (Line line, int pos, string s)
1971 bool carriage_return = false;
1973 if (s.EndsWith ("\r")) {
1974 s = s.Substring (0, s.Length - 1);
1975 carriage_return = true;
1978 InsertString (line, pos, s);
1980 if (carriage_return) {
1981 Line l = GetLine (line.line_no);
1982 l.carriage_return = true;
1986 // Insert multi-line text at the given position; use formatting at insertion point for inserted text
1987 internal void Insert(Line line, int pos, bool update_caret, string s) {
1992 LineTag tag = LineTag.FindTag (line, pos);
1996 base_line = line.line_no;
1997 old_line_count = lines;
1999 break_index = s.IndexOf ('\n');
2001 // Bump the text at insertion point a line down if we're inserting more than one line
2002 if (break_index > -1) {
2004 line.soft_break = false;
2005 // Remainder of start line is now in base_line + 1
2008 if (break_index == -1)
2009 break_index = s.Length;
2011 InsertLineString (line, pos, s.Substring (0, break_index));
2014 while (break_index < s.Length) {
2016 int next_break = s.IndexOf ('\n', break_index);
2017 int adjusted_next_break;
2018 bool carriage_return = false;
2020 if (next_break == -1) {
2021 next_break = s.Length;
2025 adjusted_next_break = next_break;
2026 if (s [next_break - 1] == '\r') {
2027 adjusted_next_break--;
2028 carriage_return = true;
2031 string line_text = s.Substring (break_index, adjusted_next_break - break_index);
2032 Add (base_line + count, line_text, line.alignment, tag.font, tag.color);
2034 if (carriage_return) {
2035 Line last = GetLine (base_line + count);
2036 last.carriage_return = true;
2039 last.soft_break = true;
2041 Line last = GetLine (base_line + count);
2042 last.soft_break = true;
2046 break_index = next_break + 1;
2049 ResumeRecalc (true);
2051 UpdateView(line, lines - old_line_count + 1, pos);
2054 // Move caret to the end of the inserted text
2055 Line l = GetLine (line.line_no + lines - old_line_count);
2056 PositionCaret(l, l.text.Length);
2061 // Inserts a character at the given position
2062 internal void InsertString(Line line, int pos, string s) {
2063 InsertString(line.FindTag(pos), pos, s);
2066 // Inserts a string at the given position
2067 internal void InsertString(LineTag tag, int pos, string s) {
2076 line.text.Insert(pos, s);
2079 while (tag != null) {
2086 UpdateView(line, pos);
2089 // Inserts a string at the caret position
2090 internal void InsertStringAtCaret(string s, bool move_caret) {
2092 InsertString (caret.tag, caret.pos, s);
2094 UpdateView(caret.line, caret.pos);
2096 caret.pos += s.Length;
2103 // Inserts a character at the given position
2104 internal void InsertChar(Line line, int pos, char ch) {
2105 InsertChar(line.FindTag(pos), pos, ch);
2108 // Inserts a character at the given position
2109 internal void InsertChar(LineTag tag, int pos, char ch) {
2115 line.text.Insert(pos, ch);
2119 while (tag != null) {
2126 undo.RecordTyping (line, pos, ch);
2127 UpdateView(line, pos);
2130 // Inserts a character at the current caret position
2131 internal void InsertCharAtCaret(char ch, bool move_caret) {
2137 caret.line.text.Insert(caret.pos, ch);
2140 if (caret.tag.next != null) {
2141 tag = caret.tag.next;
2142 while (tag != null) {
2148 caret.line.recalc = true;
2150 InsertChar (caret.tag, caret.pos, ch);
2152 UpdateView(caret.line, caret.pos);
2156 SetSelectionToCaret(true);
2161 internal void InsertImage (LineTag tag, int pos, Image image)
2169 line.text.Insert (pos, "I");
2171 LineTag next_tag = tag.Break (pos);
2172 ImageTag image_tag = new ImageTag (line, pos, image);
2173 image_tag.CopyFormattingFrom (tag);
2174 image_tag.next = next_tag;
2175 image_tag.previous = tag;
2176 tag.next = image_tag;
2178 tag = image_tag.next;
2179 while (tag != null) {
2188 UpdateView (line, pos);
2191 internal void DeleteMultiline (Line start_line, int pos, int length)
2193 Marker start = new Marker ();
2194 Marker end = new Marker ();
2195 int start_index = LineTagToCharIndex (start_line, pos);
2197 start.line = start_line;
2199 start.tag = LineTag.FindTag (start_line, pos);
2201 CharIndexToLineTag (start_index + length, out end.line,
2202 out end.tag, out end.pos);
2206 if (start.line == end.line) {
2207 DeleteChars (start.tag, pos, end.pos - pos);
2210 // Delete first and last lines
2211 DeleteChars (start.tag, start.pos, start.line.text.Length - start.pos);
2212 DeleteChars (end.line.tags, 0, end.pos);
2214 int current = start.line.line_no + 1;
2215 if (current < end.line.line_no) {
2216 for (int i = end.line.line_no - 1; i >= current; i--) {
2221 // BIG FAT WARNING - selection_end.line might be stale due
2222 // to the above Delete() call. DONT USE IT before hitting the end of this method!
2224 // Join start and end
2225 Combine (start.line.line_no, current);
2228 ResumeUpdate (true);
2232 // Deletes n characters at the given position; it will not delete past line limits
2234 internal void DeleteChars(LineTag tag, int pos, int count) {
2243 if (pos == line.text.Length) {
2247 line.text.Remove(pos, count);
2249 // Make sure the tag points to the right spot
2250 while ((tag != null) && (tag.end) < pos) {
2258 // Check if we're crossing tag boundaries
2259 if ((pos + count) > (tag.start + tag.length - 1)) {
2262 // We have to delete cross tag boundaries
2266 left -= tag.start + tag.length - pos - 1;
2269 while ((tag != null) && (left > 0)) {
2270 tag.start -= count - left;
2272 if (tag.length > left) {
2281 // We got off easy, same tag
2283 if (tag.length == 0) {
2288 // Delete empty orphaned tags at the end
2290 while (walk != null && walk.next != null && walk.next.length == 0) {
2292 walk.next = walk.next.next;
2293 if (walk.next != null)
2294 walk.next.previous = t;
2298 // Adjust the start point of any tags following
2301 while (tag != null) {
2309 line.Streamline(lines);
2312 UpdateView(line, pos);
2315 // Deletes a character at or after the given position (depending on forward); it will not delete past line limits
2316 internal void DeleteChar(LineTag tag, int pos, bool forward) {
2325 if ((pos == 0 && forward == false) || (pos == line.text.Length && forward == true)) {
2331 line.text.Remove(pos, 1);
2333 while ((tag != null) && (tag.start + tag.length - 1) <= pos) {
2343 if (tag.length == 0) {
2348 line.text.Remove(pos, 1);
2349 if (pos >= (tag.start - 1)) {
2351 if (tag.length == 0) {
2354 } else if (tag.previous != null) {
2355 // tag.previous.length--;
2356 if (tag.previous.length == 0) {
2362 // Delete empty orphaned tags at the end
2364 while (walk != null && walk.next != null && walk.next.length == 0) {
2366 walk.next = walk.next.next;
2367 if (walk.next != null)
2368 walk.next.previous = t;
2373 while (tag != null) {
2379 line.Streamline(lines);
2382 UpdateView(line, pos);
2385 // Combine two lines
2386 internal void Combine(int FirstLine, int SecondLine) {
2387 Combine(GetLine(FirstLine), GetLine(SecondLine));
2390 internal void Combine(Line first, Line second) {
2394 // Combine the two tag chains into one
2397 // Maintain the line ending style
2398 first.soft_break = second.soft_break;
2400 while (last.next != null) {
2404 // need to get the shift before setting the next tag since that effects length
2405 shift = last.start + last.length - 1;
2406 last.next = second.tags;
2407 last.next.previous = last;
2409 // Fix up references within the chain
2411 while (last != null) {
2413 last.start += shift;
2417 // Combine both lines' strings
2418 first.text.Insert(first.text.Length, second.text.ToString());
2419 first.Grow(first.text.Length);
2421 // Remove the reference to our (now combined) tags from the doomed line
2425 DecrementLines(first.line_no + 2); // first.line_no + 1 will be deleted, so we need to start renumbering one later
2428 first.recalc = true;
2429 first.height = 0; // This forces RecalcDocument/UpdateView to redraw from this line on
2430 first.Streamline(lines);
2432 // Update Caret, Selection, etc
2433 if (caret.line == second) {
2434 caret.Combine(first, shift);
2436 if (selection_anchor.line == second) {
2437 selection_anchor.Combine(first, shift);
2439 if (selection_start.line == second) {
2440 selection_start.Combine(first, shift);
2442 if (selection_end.line == second) {
2443 selection_end.Combine(first, shift);
2450 check_first = GetLine(first.line_no);
2451 check_second = GetLine(check_first.line_no + 1);
2453 Console.WriteLine("Pre-delete: Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
2456 this.Delete(second);
2459 check_first = GetLine(first.line_no);
2460 check_second = GetLine(check_first.line_no + 1);
2462 Console.WriteLine("Post-delete Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
2466 // Split the line at the position into two
2467 internal void Split(int LineNo, int pos) {
2471 line = GetLine(LineNo);
2472 tag = LineTag.FindTag(line, pos);
2473 Split(line, tag, pos, false);
2476 internal void Split(Line line, int pos) {
2479 tag = LineTag.FindTag(line, pos);
2480 Split(line, tag, pos, false);
2483 ///<summary>Split line at given tag and position into two lines</summary>
2484 ///<param name="soft">True if the split should be marked as 'soft', indicating that it can be contracted
2485 ///if more space becomes available on previous line</param>
2486 internal void Split(Line line, LineTag tag, int pos, bool soft) {
2490 bool move_sel_start;
2494 move_sel_start = false;
2495 move_sel_end = false;
2497 // Adjust selection and cursors
2498 if (caret.line == line && caret.pos >= pos) {
2501 if (selection_start.line == line && selection_start.pos > pos) {
2502 move_sel_start = true;
2505 if (selection_end.line == line && selection_end.pos > pos) {
2506 move_sel_end = true;
2509 // cover the easy case first
2510 if (pos == line.text.Length) {
2511 Add(line.line_no + 1, "", line.alignment, tag.font, tag.color);
2513 new_line = GetLine(line.line_no + 1);
2515 line.carriage_return = false;
2516 new_line.carriage_return = line.carriage_return;
2517 new_line.soft_break = soft;
2520 caret.line = new_line;
2521 caret.tag = new_line.tags;
2525 if (move_sel_start) {
2526 selection_start.line = new_line;
2527 selection_start.pos = 0;
2528 selection_start.tag = new_line.tags;
2532 selection_end.line = new_line;
2533 selection_end.pos = 0;
2534 selection_end.tag = new_line.tags;
2539 // We need to move the rest of the text into the new line
2540 Add (line.line_no + 1, line.text.ToString (pos, line.text.Length - pos), line.alignment, tag.font, tag.color);
2542 // Now transfer our tags from this line to the next
2543 new_line = GetLine(line.line_no + 1);
2545 line.carriage_return = false;
2546 new_line.carriage_return = line.carriage_return;
2547 new_line.soft_break = soft;
2550 new_line.recalc = true;
2552 if ((tag.start - 1) == pos) {
2555 // We can simply break the chain and move the tag into the next line
2556 if (tag == line.tags) {
2557 new_tag = new LineTag(line, 1);
2558 new_tag.CopyFormattingFrom (tag);
2559 line.tags = new_tag;
2562 if (tag.previous != null) {
2563 tag.previous.next = null;
2565 new_line.tags = tag;
2566 tag.previous = null;
2567 tag.line = new_line;
2569 // Walk the list and correct the start location of the tags we just bumped into the next line
2570 shift = tag.start - 1;
2573 while (new_tag != null) {
2574 new_tag.start -= shift;
2575 new_tag.line = new_line;
2576 new_tag = new_tag.next;
2581 new_tag = new LineTag (new_line, 1);
2582 new_tag.next = tag.next;
2583 new_tag.CopyFormattingFrom (tag);
2584 new_line.tags = new_tag;
2585 if (new_tag.next != null) {
2586 new_tag.next.previous = new_tag;
2591 new_tag = new_tag.next;
2592 while (new_tag != null) {
2593 new_tag.start -= shift;
2594 new_tag.line = new_line;
2595 new_tag = new_tag.next;
2601 caret.line = new_line;
2602 caret.pos = caret.pos - pos;
2603 caret.tag = caret.line.FindTag(caret.pos);
2606 if (move_sel_start) {
2607 selection_start.line = new_line;
2608 selection_start.pos = selection_start.pos - pos;
2609 selection_start.tag = new_line.FindTag(selection_start.pos);
2613 selection_end.line = new_line;
2614 selection_end.pos = selection_end.pos - pos;
2615 selection_end.tag = new_line.FindTag(selection_end.pos);
2618 CharCount -= line.text.Length - pos;
2619 line.text.Remove(pos, line.text.Length - pos);
2622 // Adds a line of text, with given font.
2623 // Bumps any line at that line number that already exists down
2624 internal void Add(int LineNo, string Text, Font font, SolidBrush color) {
2625 Add(LineNo, Text, HorizontalAlignment.Left, font, color);
2628 internal void Add(int LineNo, string Text, HorizontalAlignment align, Font font, SolidBrush color) {
2633 CharCount += Text.Length;
2635 if (LineNo<1 || Text == null) {
2637 throw new ArgumentNullException("LineNo", "Line numbers must be positive");
2639 throw new ArgumentNullException("Text", "Cannot insert NULL line");
2643 add = new Line(LineNo, Text, align, font, color);
2646 while (line != sentinel) {
2648 line_no = line.line_no;
2650 if (LineNo > line_no) {
2652 } else if (LineNo < line_no) {
2655 // Bump existing line numbers; walk all nodes to the right of this one and increment line_no
2656 IncrementLines(line.line_no);
2661 add.left = sentinel;
2662 add.right = sentinel;
2664 if (add.parent != null) {
2665 if (LineNo > add.parent.line_no) {
2666 add.parent.right = add;
2668 add.parent.left = add;
2675 RebalanceAfterAdd(add);
2680 internal virtual void Clear() {
2683 document = sentinel;
2686 public virtual object Clone() {
2689 clone = new Document(null);
2691 clone.lines = this.lines;
2692 clone.document = (Line)document.Clone();
2697 internal void Delete(int LineNo) {
2704 line = GetLine(LineNo);
2706 CharCount -= line.text.Length;
2708 DecrementLines(LineNo + 1);
2712 internal void Delete(Line line1) {
2713 Line line2;// = new Line();
2716 if ((line1.left == sentinel) || (line1.right == sentinel)) {
2719 line3 = line1.right;
2720 while (line3.left != sentinel) {
2725 if (line3.left != sentinel) {
2728 line2 = line3.right;
2731 line2.parent = line3.parent;
2732 if (line3.parent != null) {
2733 if(line3 == line3.parent.left) {
2734 line3.parent.left = line2;
2736 line3.parent.right = line2;
2742 if (line3 != line1) {
2745 if (selection_start.line == line3) {
2746 selection_start.line = line1;
2749 if (selection_end.line == line3) {
2750 selection_end.line = line1;
2753 if (selection_anchor.line == line3) {
2754 selection_anchor.line = line1;
2757 if (caret.line == line3) {
2762 line1.alignment = line3.alignment;
2763 line1.ascent = line3.ascent;
2764 line1.hanging_indent = line3.hanging_indent;
2765 line1.height = line3.height;
2766 line1.indent = line3.indent;
2767 line1.line_no = line3.line_no;
2768 line1.recalc = line3.recalc;
2769 line1.right_indent = line3.right_indent;
2770 line1.soft_break = line3.soft_break;
2771 line1.space = line3.space;
2772 line1.tags = line3.tags;
2773 line1.text = line3.text;
2774 line1.widths = line3.widths;
2778 while (tag != null) {
2784 if (line3.color == LineColor.Black)
2785 RebalanceAfterDelete(line2);
2790 // Invalidate a section of the document to trigger redraw
2791 internal void Invalidate(Line start, int start_pos, Line end, int end_pos) {
2797 if ((start == end) && (start_pos == end_pos)) {
2801 if (end_pos == -1) {
2802 end_pos = end.text.Length;
2805 // figure out what's before what so the logic below is straightforward
2806 if (start.line_no < end.line_no) {
2812 } else if (start.line_no > end.line_no) {
2819 if (start_pos < end_pos) {
2833 int endpoint = (int) l1.widths [p2];
2834 if (p2 == l1.text.Length + 1) {
2835 endpoint = (int) viewport_width;
2839 Console.WriteLine("Invaliding backwards from {0}:{1} to {2}:{3} {4}",
2840 l1.line_no, p1, l2.line_no, p2,
2842 (int)l1.widths[p1] + l1.align_shift - viewport_x,
2850 owner.Invalidate(new Rectangle (
2851 (int)l1.widths[p1] + l1.align_shift - viewport_x,
2853 endpoint - (int)l1.widths[p1] + 1,
2859 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);
2860 Console.WriteLine ("invalidate start line: {0} position: {1}", l1.text, p1);
2863 // Three invalidates:
2864 // First line from start
2865 owner.Invalidate(new Rectangle((int)l1.widths[p1] + l1.align_shift - viewport_x, l1.Y - viewport_y, viewport_width, l1.height));
2869 if ((l1.line_no + 1) < l2.line_no) {
2872 y = GetLine(l1.line_no + 1).Y;
2873 owner.Invalidate(new Rectangle(0, y - viewport_y, viewport_width, l2.Y - y));
2876 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, l2.Y - y);
2882 owner.Invalidate(new Rectangle((int)l2.widths[0] + l2.align_shift - viewport_x, l2.Y - viewport_y, (int)l2.widths[p2] + 1, l2.height));
2884 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);
2889 /// <summary>Select text around caret</summary>
2890 internal void ExpandSelection(CaretSelection mode, bool to_caret) {
2892 // We're expanding the selection to the caret position
2894 case CaretSelection.Line: {
2895 // Invalidate the selection delta
2896 if (caret > selection_prev) {
2897 Invalidate(selection_prev.line, 0, caret.line, caret.line.text.Length);
2899 Invalidate(selection_prev.line, selection_prev.line.text.Length, caret.line, 0);
2902 if (caret.line.line_no <= selection_anchor.line.line_no) {
2903 selection_start.line = caret.line;
2904 selection_start.tag = caret.line.tags;
2905 selection_start.pos = 0;
2907 selection_end.line = selection_anchor.line;
2908 selection_end.tag = selection_anchor.tag;
2909 selection_end.pos = selection_anchor.pos;
2911 selection_end_anchor = true;
2913 selection_start.line = selection_anchor.line;
2914 selection_start.pos = selection_anchor.height;
2915 selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height);
2917 selection_end.line = caret.line;
2918 selection_end.tag = caret.line.tags;
2919 selection_end.pos = caret.line.text.Length;
2921 selection_end_anchor = false;
2923 selection_prev.line = caret.line;
2924 selection_prev.tag = caret.tag;
2925 selection_prev.pos = caret.pos;
2930 case CaretSelection.Word: {
2934 start_pos = FindWordSeparator(caret.line, caret.pos, false);
2935 end_pos = FindWordSeparator(caret.line, caret.pos, true);
2938 // Invalidate the selection delta
2939 if (caret > selection_prev) {
2940 Invalidate(selection_prev.line, selection_prev.pos, caret.line, end_pos);
2942 Invalidate(selection_prev.line, selection_prev.pos, caret.line, start_pos);
2944 if (caret < selection_anchor) {
2945 selection_start.line = caret.line;
2946 selection_start.tag = caret.line.FindTag(start_pos);
2947 selection_start.pos = start_pos;
2949 selection_end.line = selection_anchor.line;
2950 selection_end.tag = selection_anchor.tag;
2951 selection_end.pos = selection_anchor.pos;
2953 selection_prev.line = caret.line;
2954 selection_prev.tag = caret.tag;
2955 selection_prev.pos = start_pos;
2957 selection_end_anchor = true;
2959 selection_start.line = selection_anchor.line;
2960 selection_start.pos = selection_anchor.height;
2961 selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height);
2963 selection_end.line = caret.line;
2964 selection_end.tag = caret.line.FindTag(end_pos);
2965 selection_end.pos = end_pos;
2967 selection_prev.line = caret.line;
2968 selection_prev.tag = caret.tag;
2969 selection_prev.pos = end_pos;
2971 selection_end_anchor = false;
2976 case CaretSelection.Position: {
2977 SetSelectionToCaret(false);
2982 // We're setting the selection 'around' the caret position
2984 case CaretSelection.Line: {
2985 this.Invalidate(caret.line, 0, caret.line, caret.line.text.Length);
2987 selection_start.line = caret.line;
2988 selection_start.tag = caret.line.tags;
2989 selection_start.pos = 0;
2991 selection_end.line = caret.line;
2992 selection_end.pos = caret.line.text.Length;
2993 selection_end.tag = caret.line.FindTag(selection_end.pos);
2995 selection_anchor.line = selection_end.line;
2996 selection_anchor.tag = selection_end.tag;
2997 selection_anchor.pos = selection_end.pos;
2998 selection_anchor.height = 0;
3000 selection_prev.line = caret.line;
3001 selection_prev.tag = caret.tag;
3002 selection_prev.pos = caret.pos;
3004 this.selection_end_anchor = true;
3009 case CaretSelection.Word: {
3013 start_pos = FindWordSeparator(caret.line, caret.pos, false);
3014 end_pos = FindWordSeparator(caret.line, caret.pos, true);
3016 this.Invalidate(selection_start.line, start_pos, caret.line, end_pos);
3018 selection_start.line = caret.line;
3019 selection_start.tag = caret.line.FindTag(start_pos);
3020 selection_start.pos = start_pos;
3022 selection_end.line = caret.line;
3023 selection_end.tag = caret.line.FindTag(end_pos);
3024 selection_end.pos = end_pos;
3026 selection_anchor.line = selection_end.line;
3027 selection_anchor.tag = selection_end.tag;
3028 selection_anchor.pos = selection_end.pos;
3029 selection_anchor.height = start_pos;
3031 selection_prev.line = caret.line;
3032 selection_prev.tag = caret.tag;
3033 selection_prev.pos = caret.pos;
3035 this.selection_end_anchor = true;
3042 SetSelectionVisible (!(selection_start == selection_end));
3045 internal void SetSelectionToCaret(bool start) {
3047 // Invalidate old selection; selection is being reset to empty
3048 this.Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3050 selection_start.line = caret.line;
3051 selection_start.tag = caret.tag;
3052 selection_start.pos = caret.pos;
3054 // start always also selects end
3055 selection_end.line = caret.line;
3056 selection_end.tag = caret.tag;
3057 selection_end.pos = caret.pos;
3059 selection_anchor.line = caret.line;
3060 selection_anchor.tag = caret.tag;
3061 selection_anchor.pos = caret.pos;
3063 // Invalidate from previous end to caret (aka new end)
3064 if (selection_end_anchor) {
3065 if (selection_start != caret) {
3066 this.Invalidate(selection_start.line, selection_start.pos, caret.line, caret.pos);
3069 if (selection_end != caret) {
3070 this.Invalidate(selection_end.line, selection_end.pos, caret.line, caret.pos);
3074 if (caret < selection_anchor) {
3075 selection_start.line = caret.line;
3076 selection_start.tag = caret.tag;
3077 selection_start.pos = caret.pos;
3079 selection_end.line = selection_anchor.line;
3080 selection_end.tag = selection_anchor.tag;
3081 selection_end.pos = selection_anchor.pos;
3083 selection_end_anchor = true;
3085 selection_start.line = selection_anchor.line;
3086 selection_start.tag = selection_anchor.tag;
3087 selection_start.pos = selection_anchor.pos;
3089 selection_end.line = caret.line;
3090 selection_end.tag = caret.tag;
3091 selection_end.pos = caret.pos;
3093 selection_end_anchor = false;
3097 SetSelectionVisible (!(selection_start == selection_end));
3100 internal void SetSelection(Line start, int start_pos, Line end, int end_pos) {
3101 if (selection_visible) {
3102 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3105 if ((end.line_no < start.line_no) || ((end == start) && (end_pos <= start_pos))) {
3106 selection_start.line = end;
3107 selection_start.tag = LineTag.FindTag(end, end_pos);
3108 selection_start.pos = end_pos;
3110 selection_end.line = start;
3111 selection_end.tag = LineTag.FindTag(start, start_pos);
3112 selection_end.pos = start_pos;
3114 selection_end_anchor = true;
3116 selection_start.line = start;
3117 selection_start.tag = LineTag.FindTag(start, start_pos);
3118 selection_start.pos = start_pos;
3120 selection_end.line = end;
3121 selection_end.tag = LineTag.FindTag(end, end_pos);
3122 selection_end.pos = end_pos;
3124 selection_end_anchor = false;
3127 selection_anchor.line = start;
3128 selection_anchor.tag = selection_start.tag;
3129 selection_anchor.pos = start_pos;
3131 if (((start == end) && (start_pos == end_pos)) || start == null || end == null) {
3132 SetSelectionVisible (false);
3134 SetSelectionVisible (true);
3135 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3139 internal void SetSelectionStart(Line start, int start_pos) {
3140 // Invalidate from the previous to the new start pos
3141 Invalidate(selection_start.line, selection_start.pos, start, start_pos);
3143 selection_start.line = start;
3144 selection_start.pos = start_pos;
3145 selection_start.tag = LineTag.FindTag(start, start_pos);
3147 selection_anchor.line = start;
3148 selection_anchor.pos = start_pos;
3149 selection_anchor.tag = selection_start.tag;
3151 selection_end_anchor = false;
3154 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
3155 SetSelectionVisible (true);
3157 SetSelectionVisible (false);
3160 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3163 internal void SetSelectionStart(int character_index) {
3168 if (character_index < 0) {
3172 CharIndexToLineTag(character_index, out line, out tag, out pos);
3173 SetSelectionStart(line, pos);
3176 internal void SetSelectionEnd(Line end, int end_pos) {
3178 if (end == selection_end.line && end_pos == selection_start.pos) {
3179 selection_anchor.line = selection_start.line;
3180 selection_anchor.tag = selection_start.tag;
3181 selection_anchor.pos = selection_start.pos;
3183 selection_end.line = selection_start.line;
3184 selection_end.tag = selection_start.tag;
3185 selection_end.pos = selection_start.pos;
3187 selection_end_anchor = false;
3188 } else if ((end.line_no < selection_anchor.line.line_no) || ((end == selection_anchor.line) && (end_pos <= selection_anchor.pos))) {
3189 selection_start.line = end;
3190 selection_start.tag = LineTag.FindTag(end, end_pos);
3191 selection_start.pos = end_pos;
3193 selection_end.line = selection_anchor.line;
3194 selection_end.tag = selection_anchor.tag;
3195 selection_end.pos = selection_anchor.pos;
3197 selection_end_anchor = true;
3199 selection_start.line = selection_anchor.line;
3200 selection_start.tag = selection_anchor.tag;
3201 selection_start.pos = selection_anchor.pos;
3203 selection_end.line = end;
3204 selection_end.tag = LineTag.FindTag(end, end_pos);
3205 selection_end.pos = end_pos;
3207 selection_end_anchor = false;
3210 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
3211 SetSelectionVisible (true);
3212 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3214 SetSelectionVisible (false);
3215 // ?? Do I need to invalidate here, tests seem to work without it, but I don't think they should :-s
3219 internal void SetSelectionEnd(int character_index) {
3224 if (character_index < 0) {
3228 CharIndexToLineTag(character_index, out line, out tag, out pos);
3229 SetSelectionEnd(line, pos);
3232 internal void SetSelection(Line start, int start_pos) {
3233 if (selection_visible) {
3234 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3237 selection_start.line = start;
3238 selection_start.pos = start_pos;
3239 selection_start.tag = LineTag.FindTag(start, start_pos);
3241 selection_end.line = start;
3242 selection_end.tag = selection_start.tag;
3243 selection_end.pos = start_pos;
3245 selection_anchor.line = start;
3246 selection_anchor.tag = selection_start.tag;
3247 selection_anchor.pos = start_pos;
3249 selection_end_anchor = false;
3250 SetSelectionVisible (false);
3253 internal void InvalidateSelectionArea() {
3254 Invalidate (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3257 // Return the current selection, as string
3258 internal string GetSelection() {
3259 // We return String.Empty if there is no selection
3260 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
3261 return string.Empty;
3264 if (!multiline || (selection_start.line == selection_end.line)) {
3265 return selection_start.line.text.ToString(selection_start.pos, selection_end.pos - selection_start.pos);
3272 sb = new StringBuilder();
3273 start = selection_start.line.line_no;
3274 end = selection_end.line.line_no;
3276 sb.Append(selection_start.line.text.ToString(selection_start.pos, selection_start.line.text.Length - selection_start.pos) + Environment.NewLine);
3278 if ((start + 1) < end) {
3279 for (i = start + 1; i < end; i++) {
3280 sb.Append(GetLine(i).text.ToString() + Environment.NewLine);
3284 sb.Append(selection_end.line.text.ToString(0, selection_end.pos));
3286 return sb.ToString();
3290 internal void ReplaceSelection(string s, bool select_new) {
3293 int selection_pos_on_line = selection_start.pos;
3294 int selection_start_pos = LineTagToCharIndex (selection_start.line, selection_start.pos);
3297 // First, delete any selected text
3298 if ((selection_start.pos != selection_end.pos) || (selection_start.line != selection_end.line)) {
3299 if (!multiline || (selection_start.line == selection_end.line)) {
3300 undo.RecordDeleteString (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3302 DeleteChars(selection_start.tag, selection_start.pos, selection_end.pos - selection_start.pos);
3304 // The tag might have been removed, we need to recalc it
3305 selection_start.tag = selection_start.line.FindTag(selection_start.pos);
3310 start = selection_start.line.line_no;
3311 end = selection_end.line.line_no;
3313 undo.RecordDeleteString (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3315 // Delete first line
3316 DeleteChars(selection_start.tag, selection_start.pos, selection_start.line.text.Length - selection_start.pos);
3319 DeleteChars(selection_end.line.tags, 0, selection_end.pos);
3323 for (i = end - 1; i >= start; i--) {
3328 // BIG FAT WARNING - selection_end.line might be stale due
3329 // to the above Delete() call. DONT USE IT before hitting the end of this method!
3331 // Join start and end
3332 Combine(selection_start.line.line_no, start);
3337 Insert(selection_start.line, selection_start.pos, false, s);
3338 undo.RecordInsertString (selection_start.line, selection_start.pos, s);
3339 ResumeRecalc (false);
3342 CharIndexToLineTag(selection_start_pos + s.Length, out selection_start.line,
3343 out selection_start.tag, out selection_start.pos);
3345 selection_end.line = selection_start.line;
3346 selection_end.pos = selection_start.pos;
3347 selection_end.tag = selection_start.tag;
3348 selection_anchor.line = selection_start.line;
3349 selection_anchor.pos = selection_start.pos;
3350 selection_anchor.tag = selection_start.tag;
3352 SetSelectionVisible (false);
3354 CharIndexToLineTag(selection_start_pos, out selection_start.line,
3355 out selection_start.tag, out selection_start.pos);
3357 CharIndexToLineTag(selection_start_pos + s.Length, out selection_end.line,
3358 out selection_end.tag, out selection_end.pos);
3360 selection_anchor.line = selection_start.line;
3361 selection_anchor.pos = selection_start.pos;
3362 selection_anchor.tag = selection_start.tag;
3364 SetSelectionVisible (true);
3367 PositionCaret (selection_start.line, selection_start.pos);
3368 UpdateView (selection_start.line, selection_pos_on_line);
3371 internal void CharIndexToLineTag(int index, out Line line_out, out LineTag tag_out, out int pos) {
3380 for (i = 1; i <= lines; i++) {
3384 chars += line.text.Length + (line.soft_break ? 0 : crlf_size);
3386 if (index <= chars) {
3387 // we found the line
3390 while (tag != null) {
3391 if (index < (start + tag.start + tag.length)) {
3393 tag_out = LineTag.GetFinalTag (tag);
3394 pos = index - start;
3397 if (tag.next == null) {
3400 next_line = GetLine(line.line_no + 1);
3402 if (next_line != null) {
3403 line_out = next_line;
3404 tag_out = LineTag.GetFinalTag (next_line.tags);
3409 tag_out = LineTag.GetFinalTag (tag);
3410 pos = line_out.text.Length;
3419 line_out = GetLine(lines);
3420 tag = line_out.tags;
3421 while (tag.next != null) {
3425 pos = line_out.text.Length;
3428 internal int LineTagToCharIndex(Line line, int pos) {
3432 // Count first and last line
3435 // Count the lines in the middle
3437 for (i = 1; i < line.line_no; i++) {
3438 length += GetLine(i).text.Length + (line.soft_break ? 0 : crlf_size);
3446 internal int SelectionLength() {
3447 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
3451 if (!multiline || (selection_start.line == selection_end.line)) {
3452 return selection_end.pos - selection_start.pos;
3459 // Count first and last line
3460 length = selection_start.line.text.Length - selection_start.pos + selection_end.pos + crlf_size;
3462 // Count the lines in the middle
3463 start = selection_start.line.line_no + 1;
3464 end = selection_end.line.line_no;
3467 for (i = start; i < end; i++) {
3468 Line line = GetLine (i);
3469 length += line.text.Length + (line.soft_break ? 0 : crlf_size);
3480 /// <summary>Give it a Line number and it returns the Line object at with that line number</summary>
3481 internal Line GetLine(int LineNo) {
3482 Line line = document;
3484 while (line != sentinel) {
3485 if (LineNo == line.line_no) {
3487 } else if (LineNo < line.line_no) {
3497 /// <summary>Retrieve the previous tag; walks line boundaries</summary>
3498 internal LineTag PreviousTag(LineTag tag) {
3501 if (tag.previous != null) {
3502 return tag.previous;
3506 if (tag.line.line_no == 1) {
3510 l = GetLine(tag.line.line_no - 1);
3515 while (t.next != null) {
3524 /// <summary>Retrieve the next tag; walks line boundaries</summary>
3525 internal LineTag NextTag(LineTag tag) {
3528 if (tag.next != null) {
3533 l = GetLine(tag.line.line_no + 1);
3541 internal Line ParagraphStart(Line line) {
3542 while (line.soft_break) {
3543 line = GetLine(line.line_no - 1);
3548 internal Line ParagraphEnd(Line line) {
3551 while (line.soft_break) {
3552 l = GetLine(line.line_no + 1);
3553 if ((l == null) || (!l.soft_break)) {
3561 /// <summary>Give it a Y pixel coordinate and it returns the Line covering that Y coordinate</summary>
3562 internal Line GetLineByPixel(int y, bool exact) {
3563 Line line = document;
3566 while (line != sentinel) {
3568 if ((y >= line.Y) && (y < (line.Y+line.height))) {
3570 } else if (y < line.Y) {
3583 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
3584 internal LineTag FindTag(int x, int y, out int index, bool exact) {
3588 line = GetLineByPixel(y, exact);
3595 // Alignment adjustment
3596 x += line.align_shift;
3599 if (x >= tag.X && x < (tag.X+tag.width)) {
3602 end = tag.start + tag.length - 1;
3604 for (int pos = tag.start; pos < end; pos++) {
3605 if (x < line.widths[pos]) {
3607 return LineTag.GetFinalTag (tag);
3611 return LineTag.GetFinalTag (tag);
3613 if (tag.next != null) {
3621 index = line.text.Length;
3622 return LineTag.GetFinalTag (tag);
3627 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
3628 internal LineTag FindCursor(int x, int y, out int index) {
3632 line = GetLineByPixel(y, false);
3635 // Adjust for alignment
3636 x -= line.align_shift;
3639 if (x >= tag.X && x < (tag.X+tag.width)) {
3644 for (int pos = tag.start-1; pos < end; pos++) {
3645 // When clicking on a character, we position the cursor to whatever edge
3646 // of the character the click was closer
3647 if (x < (line.widths[pos] + ((line.widths[pos+1]-line.widths[pos])/2))) {
3653 return LineTag.GetFinalTag (tag);
3655 if (tag.next != null) {
3658 index = line.text.Length;
3659 return LineTag.GetFinalTag (tag);
3664 /// <summary>Format area of document in specified font and color</summary>
3665 /// <param name="start_pos">1-based start position on start_line</param>
3666 /// <param name="end_pos">1-based end position on end_line </param>
3667 internal void FormatText (Line start_line, int start_pos, Line end_line, int end_pos, Font font,
3668 SolidBrush color, SolidBrush back_color, FormatSpecified specified)
3672 // First, format the first line
3673 if (start_line != end_line) {
3675 LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, font, color, back_color, specified);
3678 LineTag.FormatText(end_line, 1, end_pos, font, color, back_color, specified);
3680 // Now all the lines inbetween
3681 for (int i = start_line.line_no + 1; i < end_line.line_no; i++) {
3683 LineTag.FormatText(l, 1, l.text.Length, font, color, back_color, specified);
3686 // Special case, single line
3687 LineTag.FormatText(start_line, start_pos, end_pos - start_pos, font, color, back_color, specified);
3691 /// <summary>Re-format areas of the document in specified font and color</summary>
3692 /// <param name="start_pos">1-based start position on start_line</param>
3693 /// <param name="end_pos">1-based end position on end_line </param>
3694 /// <param name="font">Font specifying attributes</param>
3695 /// <param name="color">Color (or NULL) to apply</param>
3696 /// <param name="apply">Attributes from font and color to apply</param>
3697 internal void FormatText(Line start_line, int start_pos, Line end_line, int end_pos, FontDefinition attributes) {
3700 // First, format the first line
3701 if (start_line != end_line) {
3703 LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, attributes);
3706 LineTag.FormatText(end_line, 1, end_pos - 1, attributes);
3708 // Now all the lines inbetween
3709 for (int i = start_line.line_no + 1; i < end_line.line_no; i++) {
3711 LineTag.FormatText(l, 1, l.text.Length, attributes);
3714 // Special case, single line
3715 LineTag.FormatText(start_line, start_pos, end_pos - start_pos, attributes);
3719 internal void RecalculateAlignments() {
3725 while (line_no <= lines) {
3726 line = GetLine(line_no);
3729 switch (line.alignment) {
3730 case HorizontalAlignment.Left:
3731 line.align_shift = 0;
3733 case HorizontalAlignment.Center:
3734 line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2;
3736 case HorizontalAlignment.Right:
3737 line.align_shift = viewport_width - (int)line.widths[line.text.Length];
3747 /// <summary>Calculate formatting for the whole document</summary>
3748 internal bool RecalculateDocument(Graphics g) {
3749 return RecalculateDocument(g, 1, this.lines, false);
3752 /// <summary>Calculate formatting starting at a certain line</summary>
3753 internal bool RecalculateDocument(Graphics g, int start) {
3754 return RecalculateDocument(g, start, this.lines, false);
3757 /// <summary>Calculate formatting within two given line numbers</summary>
3758 internal bool RecalculateDocument(Graphics g, int start, int end) {
3759 return RecalculateDocument(g, start, end, false);
3762 /// <summary>With optimize on, returns true if line heights changed</summary>
3763 internal bool RecalculateDocument(Graphics g, int start, int end, bool optimize) {
3771 if (recalc_suspended > 0) {
3772 recalc_pending = true;
3773 recalc_start = Math.Min (recalc_start, start);
3774 recalc_end = Math.Max (recalc_end, end);
3775 recalc_optimize = optimize;
3779 // Fixup the positions, they can go kinda nuts
3780 start = Math.Max (start, 1);
3781 end = Math.Min (end, lines);
3783 Y = GetLine(start).Y;
3788 changed = true; // We always return true if we run non-optimized
3793 while (line_no <= (end + this.lines - shift)) {
3794 line = GetLine(line_no++);
3799 line.RecalculateLine(g, this);
3801 if (line.recalc && line.RecalculateLine(g, this)) {
3803 // If the height changed, all subsequent lines change
3810 line.RecalculatePasswordLine(g, this);
3812 if (line.recalc && line.RecalculatePasswordLine(g, this)) {
3814 // If the height changed, all subsequent lines change
3821 if (line.widths[line.text.Length] > new_width) {
3822 new_width = (int)line.widths[line.text.Length];
3825 // Calculate alignment
3826 if (line.alignment != HorizontalAlignment.Left) {
3827 if (line.alignment == HorizontalAlignment.Center) {
3828 line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2;
3830 line.align_shift = viewport_width - (int)line.widths[line.text.Length] - 1;
3836 if (line_no > lines) {
3841 if (document_x != new_width) {
3842 document_x = new_width;
3843 if (WidthChanged != null) {
3844 WidthChanged(this, null);
3848 RecalculateAlignments();
3850 line = GetLine(lines);
3852 if (document_y != line.Y + line.height) {
3853 document_y = line.Y + line.height;
3854 if (HeightChanged != null) {
3855 HeightChanged(this, null);
3862 internal int Size() {
3866 private void owner_HandleCreated(object sender, EventArgs e) {
3867 RecalculateDocument(owner.CreateGraphicsInternal());
3871 private void owner_VisibleChanged(object sender, EventArgs e) {
3872 if (owner.Visible) {
3873 RecalculateDocument(owner.CreateGraphicsInternal());
3877 internal static bool IsWordSeparator(char ch) {
3891 internal int FindWordSeparator(Line line, int pos, bool forward) {
3894 len = line.text.Length;
3897 for (int i = pos + 1; i < len; i++) {
3898 if (IsWordSeparator(line.Text[i])) {
3904 for (int i = pos - 1; i > 0; i--) {
3905 if (IsWordSeparator(line.Text[i - 1])) {
3913 /* Search document for text */
3914 internal bool FindChars(char[] chars, Marker start, Marker end, out Marker result) {
3920 // Search for occurence of any char in the chars array
3921 result = new Marker();
3924 line_no = start.line.line_no;
3926 while (line_no <= end.line.line_no) {
3927 line_len = line.text.Length;
3928 while (pos < line_len) {
3929 for (int i = 0; i < chars.Length; i++) {
3930 if (line.text[pos] == chars[i]) {
3932 if ((line.line_no == end.line.line_no) && (pos >= end.pos)) {
3946 line = GetLine(line_no);
3952 // This version does not build one big string for searching, instead it handles
3953 // line-boundaries, which is faster and less memory intensive
3954 // FIXME - Depending on culture stuff we might have to create a big string and use culturespecific
3955 // search stuff and change it to accept and return positions instead of Markers (which would match
3956 // RichTextBox behaviour better but would be inconsistent with the rest of TextControl)
3957 internal bool Find(string search, Marker start, Marker end, out Marker result, RichTextBoxFinds options) {
3959 string search_string;
3971 result = new Marker();
3972 word_option = ((options & RichTextBoxFinds.WholeWord) != 0);
3973 ignore_case = ((options & RichTextBoxFinds.MatchCase) == 0);
3974 reverse = ((options & RichTextBoxFinds.Reverse) != 0);
3977 line_no = start.line.line_no;
3981 // Prep our search string, lowercasing it if we do case-independent matching
3984 sb = new StringBuilder(search);
3985 for (int i = 0; i < sb.Length; i++) {
3986 sb[i] = Char.ToLower(sb[i]);
3988 search_string = sb.ToString();
3990 search_string = search;
3993 // We need to check if the character before our start position is a wordbreak
3996 if ((pos == 0) || (IsWordSeparator(line.text[pos - 1]))) {
4003 if (IsWordSeparator(line.text[pos - 1])) {
4009 // Need to check the end of the previous line
4012 prev_line = GetLine(line_no - 1);
4013 if (prev_line.soft_break) {
4014 if (IsWordSeparator(prev_line.text[prev_line.text.Length - 1])) {
4028 // To avoid duplication of this loop with reverse logic, we search
4029 // through the document, remembering the last match and when returning
4030 // report that last remembered match
4032 last = new Marker();
4033 last.height = -1; // Abused - we use it to track change
4035 while (line_no <= end.line.line_no) {
4036 if (line_no != end.line.line_no) {
4037 line_len = line.text.Length;
4042 while (pos < line_len) {
4043 if (word_option && (current == search_string.Length)) {
4044 if (IsWordSeparator(line.text[pos])) {
4057 c = Char.ToLower(line.text[pos]);
4062 if (c == search_string[current]) {
4067 if (!word_option || (word_option && (word || (current > 0)))) {
4071 if (!word_option && (current == search_string.Length)) {
4088 if (IsWordSeparator(c)) {
4096 // Mark that we just saw a word boundary
4097 if (!line.soft_break) {
4101 if (current == search_string.Length) {
4117 line = GetLine(line_no);
4121 if (last.height != -1) {
4131 // if ((line.line_no == end.line.line_no) && (pos >= end.pos)) {
4143 internal void GetMarker(out Marker mark, bool start) {
4144 mark = new Marker();
4147 mark.line = GetLine(1);
4148 mark.tag = mark.line.tags;
4151 mark.line = GetLine(lines);
4152 mark.tag = mark.line.tags;
4153 while (mark.tag.next != null) {
4154 mark.tag = mark.tag.next;
4156 mark.pos = mark.line.text.Length;
4159 #endregion // Internal Methods
4162 internal event EventHandler CaretMoved;
4163 internal event EventHandler WidthChanged;
4164 internal event EventHandler HeightChanged;
4165 internal event EventHandler LengthChanged;
4166 #endregion // Events
4168 #region Administrative
4169 public IEnumerator GetEnumerator() {
4174 public override bool Equals(object obj) {
4179 if (!(obj is Document)) {
4187 if (ToString().Equals(((Document)obj).ToString())) {
4194 public override int GetHashCode() {
4198 public override string ToString() {
4199 return "document " + this.document_id;
4201 #endregion // Administrative
4204 internal class ImageTag : LineTag {
4206 internal Image image;
4208 internal ImageTag (Line line, int start, Image image) : base (line, start)
4213 internal override SizeF SizeOfPosition (Graphics dc, int pos)
4218 internal override int MaxHeight ()
4220 return image.Height;
4223 internal override void Draw (Graphics dc, Brush brush, float x, float y, int start, int end)
4225 dc.DrawImage (image, x, y);
4229 internal class LineTag {
4230 #region Local Variables;
4231 // Payload; formatting
4232 internal Font font; // System.Drawing.Font object for this tag
4233 internal SolidBrush color; // The font color for this tag
4235 // In 2.0 tags can have background colours. I'm not going to #ifdef
4236 // at this level though since I want to reduce code paths
4237 internal SolidBrush back_color;
4240 internal int start; // start, in chars; index into Line.text
4241 internal bool r_to_l; // Which way is the font
4244 internal int height; // Height in pixels of the text this tag describes
4246 internal int ascent; // Ascent of the font for this tag
4247 internal int shift; // Shift down for this tag, to stay on baseline
4250 internal Line line; // The line we're on
4251 internal LineTag next; // Next tag on the same line
4252 internal LineTag previous; // Previous tag on the same line
4255 #region Constructors
4256 internal LineTag(Line line, int start) {
4260 #endregion // Constructors
4262 #region Internal Methods
4268 return line.widths [start - 1];
4273 get { return start + length; }
4276 public float width {
4280 return line.widths [start + length - 1] - line.widths [start - 1];
4288 res = next.start - start;
4290 res = line.text.Length - (start - 1);
4292 return res > 0 ? res : 0;
4296 internal virtual SizeF SizeOfPosition (Graphics dc, int pos)
4298 return dc.MeasureString (line.text.ToString (pos, 1), font, 10000, Document.string_format);
4301 internal virtual int MaxHeight ()
4306 internal virtual void Draw (Graphics dc, Brush brush, float x, float y, int start, int end)
4308 dc.DrawString (line.text.ToString (start, end), font, brush, x, y, StringFormat.GenericTypographic);
4311 internal virtual void Draw (Graphics dc, Brush brush, float x, float y, int start, int end, string text) {
4312 dc.DrawString (text.Substring (start, end), font, brush, x, y, StringFormat.GenericTypographic);
4315 ///<summary>Break a tag into two with identical attributes; pos is 1-based; returns tag starting at >pos< or null if end-of-line</summary>
4316 internal LineTag Break(int pos) {
4321 if (pos == this.start) {
4323 } else if (pos >= (start + length)) {
4327 new_tag = new LineTag(line, pos);
4328 new_tag.CopyFormattingFrom (this);
4330 new_tag.next = this.next;
4331 this.next = new_tag;
4332 new_tag.previous = this;
4334 if (new_tag.next != null) {
4335 new_tag.next.previous = new_tag;
4341 public string Text ()
4343 return line.text.ToString (start - 1, length);
4346 public void CopyFormattingFrom (LineTag other)
4348 height = other.height;
4350 color = other.color;
4351 back_color = other.back_color;
4354 ///<summary>Create new font and brush from existing font and given new attributes. Returns true if fontheight changes</summary>
4355 internal static bool GenerateTextFormat(Font font_from, SolidBrush color_from, FontDefinition attributes, out Font new_font, out SolidBrush new_color) {
4361 if (attributes.font_obj == null) {
4362 size = font_from.SizeInPoints;
4363 unit = font_from.Unit;
4364 face = font_from.Name;
4365 style = font_from.Style;
4367 if (attributes.face != null) {
4368 face = attributes.face;
4371 if (attributes.size != 0) {
4372 size = attributes.size;
4375 style |= attributes.add_style;
4376 style &= ~attributes.remove_style;
4379 new_font = new Font(face, size, style, unit);
4381 new_font = attributes.font_obj;
4384 // Create 'new' color brush
4385 if (attributes.color != Color.Empty) {
4386 new_color = new SolidBrush(attributes.color);
4388 new_color = color_from;
4391 if (new_font.Height == font_from.Height) {
4397 /// <summary>Applies 'font' and 'brush' to characters starting at 'start' for 'length' chars;
4398 /// Removes any previous tags overlapping the same area;
4399 /// returns true if lineheight has changed</summary>
4400 /// <param name="start">1-based character position on line</param>
4401 internal static bool FormatText(Line line, int start, int length, Font font, SolidBrush color, SolidBrush back_color, FormatSpecified specified)
4407 bool retval = false; // Assume line-height doesn't change
4410 if (((FormatSpecified.Font & specified) == FormatSpecified.Font) && font.Height != line.height) {
4413 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
4415 // A little sanity, not sure if it's needed, might be able to remove for speed
4416 if (length > line.text.Length) {
4417 length = line.text.Length;
4421 end = start + length;
4423 // Common special case
4424 if ((start == 1) && (length == tag.length)) {
4426 SetFormat (tag, font, color, back_color, specified);
4430 start_tag = FindTag (line, start);
4432 tag = start_tag.Break (start);
4434 while (tag != null && tag.end <= end) {
4435 SetFormat (tag, font, color, back_color, specified);
4439 if (end != line.text.Length) {
4440 /// Now do the last tag
4441 end_tag = FindTag (line, end);
4443 if (end_tag != null) {
4444 end_tag.Break (end);
4445 SetFormat (end_tag, font, color, back_color, specified);
4452 private static void SetFormat (LineTag tag, Font font, SolidBrush color, SolidBrush back_color, FormatSpecified specified)
4454 if ((FormatSpecified.Font & specified) == FormatSpecified.Font)
4456 if ((FormatSpecified.Color & specified) == FormatSpecified.Color)
4458 if ((FormatSpecified.BackColor & specified) == FormatSpecified.BackColor) {
4459 tag.back_color = back_color;
4461 // Console.WriteLine ("setting format: {0} {1} new color {2}", color.Color, specified, tag.color.Color);
4464 /// <summary>Applies font attributes specified to characters starting at 'start' for 'length' chars;
4465 /// Breaks tags at start and end point, keeping middle tags with altered attributes.
4466 /// Returns true if lineheight has changed</summary>
4467 /// <param name="start">1-based character position on line</param>
4468 internal static bool FormatText(Line line, int start, int length, FontDefinition attributes) {
4472 bool retval = false; // Assume line-height doesn't change
4474 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
4476 // A little sanity, not sure if it's needed, might be able to remove for speed
4477 if (length > line.text.Length) {
4478 length = line.text.Length;
4483 // Common special case
4484 if ((start == 1) && (length == tag.length)) {
4486 GenerateTextFormat(tag.font, tag.color, attributes, out tag.font, out tag.color);
4490 start_tag = FindTag(line, start);
4492 if (start_tag == null) {
4494 // We are 'starting' after all valid tags; create a new tag with the right attributes
4495 start_tag = FindTag(line, line.text.Length - 1);
4496 start_tag.next = new LineTag(line, line.text.Length + 1);
4497 start_tag.next.CopyFormattingFrom (start_tag);
4498 start_tag.next.previous = start_tag;
4499 start_tag = start_tag.next;
4501 throw new Exception(String.Format("Could not find start_tag in document at line {0} position {1}", line.line_no, start));
4504 start_tag = start_tag.Break(start);
4507 end_tag = FindTag(line, start + length);
4508 if (end_tag != null) {
4509 end_tag = end_tag.Break(start + length);
4512 // start_tag or end_tag might be null; we're cool with that
4513 // we now walk from start_tag to end_tag, applying new attributes
4515 while ((tag != null) && tag != end_tag) {
4516 if (LineTag.GenerateTextFormat(tag.font, tag.color, attributes, out tag.font, out tag.color)) {
4525 /// <summary>Finds the tag that describes the character at position 'pos' on 'line'</summary>
4526 internal static LineTag FindTag(Line line, int pos) {
4527 LineTag tag = line.tags;
4529 // Beginning of line is a bit special
4531 // Not sure if we should get the final tag here
4535 while (tag != null) {
4536 if ((tag.start <= pos) && (pos <= tag.end)) {
4537 return GetFinalTag (tag);
4546 // There can be multiple tags at the same position, we want to make
4547 // sure we are using the very last tag at the given position
4548 internal static LineTag GetFinalTag (LineTag tag)
4552 while (res.next != null && res.next.length == 0)
4557 /// <summary>Combines 'this' tag with 'other' tag</summary>
4558 internal bool Combine(LineTag other) {
4559 if (!this.Equals(other)) {
4563 this.next = other.next;
4564 if (this.next != null) {
4565 this.next.previous = this;
4572 /// <summary>Remove 'this' tag ; to be called when formatting is to be removed</summary>
4573 internal bool Remove() {
4574 if ((this.start == 1) && (this.next == null)) {
4575 // We cannot remove the only tag
4578 if (this.start != 1) {
4579 this.previous.next = this.next;
4580 this.next.previous = this.previous;
4582 this.next.start = 1;
4583 this.line.tags = this.next;
4584 this.next.previous = null;
4590 /// <summary>Checks if 'this' tag describes the same formatting options as 'obj'</summary>
4591 public override bool Equals(object obj) {
4598 if (!(obj is LineTag)) {
4606 other = (LineTag)obj;
4608 if (this.font.Equals(other.font) && this.color.Equals(other.color)) { // FIXME add checking for things like link or type later
4615 public override int GetHashCode() {
4616 return base.GetHashCode ();
4619 public override string ToString() {
4621 return "Tag starts at index " + this.start + " length " + this.length + " text: " + Text () + "Font " + this.font.ToString();
4622 return "Zero Lengthed tag at index " + this.start;
4625 #endregion // Internal Methods
4628 internal class UndoManager {
4630 internal enum ActionType {
4634 // This is basically just cut & paste
4642 internal class Action {
4643 internal ActionType type;
4644 internal int line_no;
4646 internal object data;
4649 #region Local Variables
4650 private Document document;
4651 private Stack undo_actions;
4652 private Stack redo_actions;
4654 private int caret_line;
4655 private int caret_pos;
4657 // When performing an action, we lock the queue, so that the action can't be undone
4658 private bool locked;
4659 #endregion // Local Variables
4661 #region Constructors
4662 internal UndoManager (Document document)
4664 this.document = document;
4665 undo_actions = new Stack (50);
4666 redo_actions = new Stack (50);
4668 #endregion // Constructors
4671 internal bool CanUndo {
4672 get { return undo_actions.Count > 0; }
4675 internal bool CanRedo {
4676 get { return redo_actions.Count > 0; }
4679 internal string UndoActionName {
4681 foreach (Action action in undo_actions) {
4682 if (action.type == ActionType.UserActionBegin)
4683 return (string) action.data;
4684 if (action.type == ActionType.Typing)
4685 return Locale.GetText ("Typing");
4687 return String.Empty;
4691 internal string RedoActionName {
4693 foreach (Action action in redo_actions) {
4694 if (action.type == ActionType.UserActionBegin)
4695 return (string) action.data;
4696 if (action.type == ActionType.Typing)
4697 return Locale.GetText ("Typing");
4699 return String.Empty;
4702 #endregion // Properties
4704 #region Internal Methods
4705 internal void Clear ()
4707 undo_actions.Clear();
4708 redo_actions.Clear();
4711 internal void Undo ()
4714 bool user_action_finished = false;
4716 if (undo_actions.Count == 0)
4719 // Nuke the redo queue
4720 redo_actions.Clear ();
4725 action = (Action) undo_actions.Pop ();
4727 // Put onto redo stack
4728 redo_actions.Push(action);
4731 switch(action.type) {
4733 case ActionType.UserActionBegin:
4734 user_action_finished = true;
4737 case ActionType.UserActionEnd:
4741 case ActionType.InsertString:
4742 start = document.GetLine (action.line_no);
4743 document.SuspendUpdate ();
4744 document.DeleteMultiline (start, action.pos, ((string) action.data).Length + 1);
4745 document.PositionCaret (start, action.pos);
4746 document.SetSelectionToCaret (true);
4747 document.ResumeUpdate (true);
4750 case ActionType.Typing:
4751 start = document.GetLine (action.line_no);
4752 document.SuspendUpdate ();
4753 document.DeleteMultiline (start, action.pos, ((StringBuilder) action.data).Length);
4754 document.PositionCaret (start, action.pos);
4755 document.SetSelectionToCaret (true);
4756 document.ResumeUpdate (true);
4758 // This is an open ended operation, so only a single typing operation can be undone at once
4759 user_action_finished = true;
4762 case ActionType.DeleteString:
4763 start = document.GetLine (action.line_no);
4764 document.SuspendUpdate ();
4765 Insert (start, action.pos, (Line) action.data, true);
4766 document.ResumeUpdate (true);
4769 } while (!user_action_finished && undo_actions.Count > 0);
4774 internal void Redo ()
4777 bool user_action_finished = false;
4779 if (redo_actions.Count == 0)
4782 // You can't undo anything after redoing
4783 undo_actions.Clear ();
4790 action = (Action) redo_actions.Pop ();
4792 switch (action.type) {
4794 case ActionType.UserActionBegin:
4798 case ActionType.UserActionEnd:
4799 user_action_finished = true;
4802 case ActionType.InsertString:
4803 start = document.GetLine (action.line_no);
4804 document.SuspendUpdate ();
4805 start_index = document.LineTagToCharIndex (start, action.pos);
4806 document.InsertString (start, action.pos, (string) action.data);
4807 document.CharIndexToLineTag (start_index + ((string) action.data).Length,
4808 out document.caret.line, out document.caret.tag,
4809 out document.caret.pos);
4810 document.UpdateCaret ();
4811 document.SetSelectionToCaret (true);
4812 document.ResumeUpdate (true);
4815 case ActionType.Typing:
4816 start = document.GetLine (action.line_no);
4817 document.SuspendUpdate ();
4818 start_index = document.LineTagToCharIndex (start, action.pos);
4819 document.InsertString (start, action.pos, ((StringBuilder) action.data).ToString ());
4820 document.CharIndexToLineTag (start_index + ((StringBuilder) action.data).Length,
4821 out document.caret.line, out document.caret.tag,
4822 out document.caret.pos);
4823 document.UpdateCaret ();
4824 document.SetSelectionToCaret (true);
4825 document.ResumeUpdate (true);
4827 // This is an open ended operation, so only a single typing operation can be undone at once
4828 user_action_finished = true;
4831 case ActionType.DeleteString:
4832 start = document.GetLine (action.line_no);
4833 document.SuspendUpdate ();
4834 document.DeleteMultiline (start, action.pos, ((Line) action.data).text.Length);
4835 document.PositionCaret (start, action.pos);
4836 document.SetSelectionToCaret (true);
4837 document.ResumeUpdate (true);
4841 } while (!user_action_finished && redo_actions.Count > 0);
4845 #endregion // Internal Methods
4847 #region Private Methods
4849 public void BeginUserAction (string name)
4854 Action ua = new Action ();
4855 ua.type = ActionType.UserActionBegin;
4858 undo_actions.Push (ua);
4861 public void EndUserAction ()
4866 Action ua = new Action ();
4867 ua.type = ActionType.UserActionEnd;
4869 undo_actions.Push (ua);
4872 // start_pos, end_pos = 1 based
4873 public void RecordDeleteString (Line start_line, int start_pos, Line end_line, int end_pos)
4878 Action a = new Action ();
4880 // We cant simply store the string, because then formatting would be lost
4881 a.type = ActionType.DeleteString;
4882 a.line_no = start_line.line_no;
4884 a.data = Duplicate (start_line, start_pos, end_line, end_pos);
4886 undo_actions.Push(a);
4889 public void RecordInsertString (Line line, int pos, string str)
4891 if (locked || str.Length == 0)
4894 Action a = new Action ();
4896 a.type = ActionType.InsertString;
4898 a.line_no = line.line_no;
4901 undo_actions.Push (a);
4904 public void RecordTyping (Line line, int pos, char ch)
4911 if (undo_actions.Count > 0)
4912 a = (Action) undo_actions.Peek ();
4914 if (a == null || a.type != ActionType.Typing) {
4916 a.type = ActionType.Typing;
4917 a.data = new StringBuilder ();
4918 a.line_no = line.line_no;
4921 undo_actions.Push (a);
4924 StringBuilder data = (StringBuilder) a.data;
4928 // start_pos = 1-based
4929 // end_pos = 1-based
4930 public Line Duplicate(Line start_line, int start_pos, Line end_line, int end_pos) {
4935 LineTag current_tag;
4943 for (int i = start_line.line_no; i <= end_line.line_no; i++) {
4944 current = document.GetLine(i);
4946 if (start_line.line_no == i) {
4952 if (end_line.line_no == i) {
4955 end = current.text.Length;
4959 line.text = new StringBuilder (current.text.ToString (start, end - start));
4961 // Copy tags from start to start+length onto new line
4962 current_tag = current.FindTag (start);
4963 while ((current_tag != null) && (current_tag.start < end)) {
4964 if ((current_tag.start <= start) && (start < (current_tag.start + current_tag.length))) {
4965 // start tag is within this tag
4968 tag_start = current_tag.start;
4971 tag = new LineTag(line, tag_start - start + 1);
4972 tag.CopyFormattingFrom (current_tag);
4974 current_tag = current_tag.next;
4976 // Add the new tag to the line
4977 if (line.tags == null) {
4983 while (tail.next != null) {
4987 tag.previous = tail;
4991 if ((i + 1) <= end_line.line_no) {
4992 line.soft_break = current.soft_break;
4994 // Chain them (we use right/left as next/previous)
4995 line.right = new Line();
4996 line.right.left = line;
5004 // Insert multi-line text at the given position; use formatting at insertion point for inserted text
5005 internal void Insert(Line line, int pos, Line insert, bool select)
5013 // Handle special case first
5014 if (insert.right == null) {
5016 // Single line insert
5017 document.Split(line, pos);
5019 if (insert.tags == null) {
5020 return; // Blank line
5023 //Insert our tags at the end
5026 while (tag.next != null) {
5030 offset = tag.start + tag.length - 1;
5032 tag.next = insert.tags;
5033 line.text.Insert(offset, insert.text.ToString());
5035 // Adjust start locations
5037 while (tag != null) {
5038 tag.start += offset;
5042 // Put it back together
5043 document.Combine(line.line_no, line.line_no + 1);
5046 document.SetSelectionStart (line, pos);
5047 document.SetSelectionEnd (line, pos + insert.text.Length);
5050 document.UpdateView(line, pos);
5058 while (current != null) {
5059 if (current == insert) {
5060 // Inserting the first line we split the line (and make space)
5061 document.Split(line, pos);
5062 //Insert our tags at the end of the line
5066 while (tag.next != null) {
5069 offset = tag.start + tag.length - 1;
5070 tag.next = current.tags;
5071 tag.next.previous = tag;
5077 line.tags = current.tags;
5078 line.tags.previous = null;
5082 document.Split(line.line_no, 0);
5084 line.tags = current.tags;
5085 line.tags.previous = null;
5088 // Adjust start locations and line pointers
5089 while (tag != null) {
5090 tag.start += offset;
5095 line.text.Insert(offset, current.text.ToString());
5096 line.Grow(line.text.Length);
5099 line = document.GetLine(line.line_no + 1);
5101 // FIXME? Test undo of line-boundaries
5102 if ((current.right == null) && (current.tags.length != 0)) {
5103 document.Combine(line.line_no - 1, line.line_no);
5105 current = current.right;
5110 // Recalculate our document
5111 document.UpdateView(first, lines, pos);
5114 #endregion // Private Methods