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, images, 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;
74 internal FontDefinition() {
81 internal enum CaretDirection {
82 CharForward, // Move a char to the right
83 CharBack, // Move a char to the left
84 LineUp, // Move a line up
85 LineDown, // Move a line down
86 Home, // Move to the beginning of the line
87 End, // Move to the end of the line
88 PgUp, // Move one page up
89 PgDn, // Move one page down
90 CtrlPgUp, // Move caret to the first visible char in the viewport
91 CtrlPgDn, // Move caret to the last visible char in the viewport
92 CtrlHome, // Move to the beginning of the document
93 CtrlEnd, // Move to the end of the document
94 WordBack, // Move to the beginning of the previous word (or beginning of line)
95 WordForward, // Move to the beginning of the next word (or end of line)
96 SelectionStart, // Move to the beginning of the current selection
97 SelectionEnd, // Move to the end of the current selection
98 CharForwardNoWrap, // Move a char forward, but don't wrap onto the next line
99 CharBackNoWrap // Move a char backward, but don't wrap onto the previous line
102 // Being cloneable should allow for nice line and document copies...
103 internal class Line : ICloneable, IComparable {
104 #region Local Variables
105 // Stuff that matters for our line
106 internal StringBuilder text; // Characters for the line
107 internal float[] widths; // Width of each character; always one larger than text.Length
108 internal int space; // Number of elements in text and widths
109 internal int line_no; // Line number
110 internal LineTag tags; // Tags describing the text
111 internal int Y; // Baseline
112 internal int height; // Height of the line (height of tallest tag)
113 internal int ascent; // Ascent of the line (ascent of the tallest tag)
114 internal HorizontalAlignment alignment; // Alignment of the line
115 internal int align_shift; // Pixel shift caused by the alignment
116 internal bool soft_break; // Tag is 'broken soft' and continuation from previous line
117 internal int indent; // Left indent for the first line
118 internal int hanging_indent; // Hanging indent (left indent for all but the first line)
119 internal int right_indent; // Right indent for all lines
122 // Stuff that's important for the tree
123 internal Line parent; // Our parent line
124 internal Line left; // Line with smaller line number
125 internal Line right; // Line with higher line number
126 internal LineColor color; // We're doing a black/red tree. this is the node color
127 internal int DEFAULT_TEXT_LEN; //
128 internal static StringFormat string_format; // For calculating widths/heights
129 internal bool recalc; // Line changed
130 #endregion // Local Variables
134 color = LineColor.Red;
141 alignment = HorizontalAlignment.Left;
143 if (string_format == null) {
144 string_format = new StringFormat(StringFormat.GenericTypographic);
145 string_format.Trimming = StringTrimming.None;
146 string_format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;
150 internal Line(int LineNo, string Text, Font font, Brush color) : this() {
151 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
153 text = new StringBuilder(Text, space);
156 widths = new float[space + 1];
157 tags = new LineTag(this, 1, text.Length);
162 internal Line(int LineNo, string Text, HorizontalAlignment align, Font font, Brush color) : this() {
163 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
165 text = new StringBuilder(Text, space);
169 widths = new float[space + 1];
170 tags = new LineTag(this, 1, text.Length);
175 internal Line(int LineNo, string Text, LineTag tag) : this() {
176 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
178 text = new StringBuilder(Text, space);
181 widths = new float[space + 1];
185 #endregion // Constructors
187 #region Internal Properties
188 internal int Indent {
199 internal int HangingIndent {
201 return hanging_indent;
205 hanging_indent = value;
210 internal int RightIndent {
216 right_indent = value;
222 internal int Height {
232 internal int LineNo {
242 internal string Text {
244 return text.ToString();
248 text = new StringBuilder(value, value.Length > DEFAULT_TEXT_LEN ? value.Length : DEFAULT_TEXT_LEN);
252 internal HorizontalAlignment Alignment {
258 if (alignment != value) {
265 internal StringBuilder Text {
275 #endregion // Internal Properties
277 #region Internal Methods
278 // Make sure we always have enoughs space in text and widths
279 internal void Grow(int minimum) {
283 length = text.Length;
285 if ((length + minimum) > space) {
286 // We need to grow; double the size
288 if ((length + minimum) > (space * 2)) {
289 new_widths = new float[length + minimum * 2 + 1];
290 space = length + minimum * 2;
292 new_widths = new float[space * 2 + 1];
295 widths.CopyTo(new_widths, 0);
301 internal void Streamline(int lines) {
308 // Catch what the loop below wont; eliminate 0 length
309 // tags, but only if there are other tags after us
310 while ((current.length == 0) && (next != null)) {
312 tags.previous = null;
321 while (next != null) {
322 // Take out 0 length tags unless it's the last tag in the document
323 if (next.length == 0) {
324 if ((next.next != null) || (line_no != lines)) {
325 current.next = next.next;
326 if (current.next != null) {
327 current.next.previous = current;
333 if (current.Combine(next)) {
338 current = current.next;
343 /// <summary> Find the tag on a line based on the character position, pos is 0-based</summary>
344 internal LineTag FindTag(int pos) {
353 if (pos >= text.Length) {
354 pos = text.Length - 1;
357 while (tag != null) {
358 if (((tag.start - 1) <= pos) && (pos < (tag.start + tag.length - 1))) {
359 return LineTag.GetFinalTag (tag);
367 /// Recalculate a single line using the same char for every character in the line
370 internal bool RecalculatePasswordLine(Graphics g, Document doc) {
379 len = this.text.Length;
389 w = g.MeasureString(doc.password_char, tags.font, 10000, string_format).Width;
391 if (this.height != (int)tag.font.Height) {
397 this.height = (int)tag.font.Height;
398 tag.height = this.height;
400 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
401 this.ascent = tag.ascent;
406 widths[pos] = widths[pos-1] + w;
413 /// Go through all tags on a line and recalculate all size-related values;
414 /// returns true if lineheight changed
416 internal bool RecalculateLine(Graphics g, Document doc) {
430 len = this.text.Length;
432 prev_height = this.height; // For drawing optimization calculations
433 this.height = 0; // Reset line height
434 this.ascent = 0; // Reset the ascent for the line
438 if (this.soft_break) {
439 widths[0] = hanging_indent;
452 size = g.MeasureString(this.text.ToString(pos, 1), tag.font, 10000, string_format);
454 while (tag.length == 0) { // We should always have tags after a tag.length==0 unless len==0
457 if (tag.previous != null) {
458 tag.X = tag.previous.X;
460 tag.X = (int)widths[pos];
469 if (Char.IsWhiteSpace(text[pos])) {
471 wrap_width = tag.width + w;
475 if ((wrap_pos > 0) && (wrap_pos != len) && (widths[pos] + w) + 27 > (doc.viewport_width - this.right_indent)) {
477 tag.width = wrap_width;
478 doc.Split(this, tag, pos, this.soft_break);
479 this.soft_break = true;
480 len = this.text.Length;
483 } else if ((widths[pos] + w) > (doc.viewport_width - this.right_indent)) {
484 // No suitable wrap position was found so break right in the middle of a word
485 tag.width = tag.width + w;
486 doc.Split(this, tag, pos, this.soft_break);
487 this.soft_break = true;
488 len = this.text.Length;
494 // Contract all soft lines that follow back into our line
500 widths[pos] = widths[pos-1] + w;
503 line = doc.GetLine(this.line_no + 1);
504 if ((line != null) && (line.soft_break)) {
505 // Pull the previous line back into this one
506 doc.Combine(this.line_no, this.line_no + 1);
507 len = this.text.Length;
513 if (pos == (tag.start-1 + tag.length)) {
514 // We just found the end of our current tag
515 tag.height = (int)tag.font.Height;
517 // Check if we're the tallest on the line (so far)
518 if (tag.height > this.height) {
519 this.height = tag.height; // Yep; make sure the line knows
522 if (tag.ascent == 0) {
525 XplatUI.GetFontMetrics(g, tag.font, out tag.ascent, out descent);
528 if (tag.ascent > this.ascent) {
531 // We have a tag that has a taller ascent than the line;
535 t.shift = tag.ascent - t.ascent;
540 this.ascent = tag.ascent;
542 tag.shift = this.ascent - tag.ascent;
545 // Update our horizontal starting pixel position
546 if (tag.previous == null) {
547 tag.X = (int)widths[0];
549 tag.X = tag.previous.X + (int)tag.previous.width;
557 wrap_width = tag.width;
562 if (this.height == 0) {
563 this.height = tags.font.Height;
564 tag.height = this.height;
567 if (prev_height != this.height) {
572 #endregion // Internal Methods
574 #region Administrative
575 public int CompareTo(object obj) {
580 if (! (obj is Line)) {
581 throw new ArgumentException("Object is not of type Line", "obj");
584 if (line_no < ((Line)obj).line_no) {
586 } else if (line_no > ((Line)obj).line_no) {
593 public object Clone() {
601 clone.left = (Line)left.Clone();
605 clone.left = (Line)left.Clone();
611 internal object CloneLine() {
621 public override bool Equals(object obj) {
626 if (!(obj is Line)) {
634 if (line_no == ((Line)obj).line_no) {
641 public override int GetHashCode() {
642 return base.GetHashCode ();
645 public override string ToString() {
646 return "Line " + line_no;
649 #endregion // Administrative
652 internal class Document : ICloneable, IEnumerable {
654 // FIXME - go through code and check for places where
655 // we do explicit comparisons instead of using the compare overloads
656 internal struct Marker {
658 internal LineTag tag;
662 public static bool operator<(Marker lhs, Marker rhs) {
663 if (lhs.line.line_no < rhs.line.line_no) {
667 if (lhs.line.line_no == rhs.line.line_no) {
668 if (lhs.pos < rhs.pos) {
675 public static bool operator>(Marker lhs, Marker rhs) {
676 if (lhs.line.line_no > rhs.line.line_no) {
680 if (lhs.line.line_no == rhs.line.line_no) {
681 if (lhs.pos > rhs.pos) {
688 public static bool operator==(Marker lhs, Marker rhs) {
689 if ((lhs.line.line_no == rhs.line.line_no) && (lhs.pos == rhs.pos)) {
695 public static bool operator!=(Marker lhs, Marker rhs) {
696 if ((lhs.line.line_no != rhs.line.line_no) || (lhs.pos != rhs.pos)) {
702 public void Combine(Line move_to_line, int move_to_line_length) {
704 pos += move_to_line_length;
705 tag = LineTag.FindTag(line, pos);
708 // This is for future use, right now Document.Split does it by hand, with some added shortcut logic
709 public void Split(Line move_to_line, int split_at) {
712 tag = LineTag.FindTag(line, pos);
715 public override bool Equals(object obj) {
716 return this==(Marker)obj;
719 public override int GetHashCode() {
720 return base.GetHashCode ();
723 public override string ToString() {
724 return "Marker Line " + line + ", Position " + pos;
728 #endregion Structures
730 #region Local Variables
731 private Line document;
733 private Line sentinel;
734 private Line last_found;
735 private int document_id;
736 private Random random = new Random();
737 internal string password_char;
738 private StringBuilder password_cache;
739 private bool calc_pass;
740 private int char_count;
742 private bool no_recalc;
743 private bool recalc_pending;
744 private int recalc_start;
745 private int recalc_end;
746 private bool recalc_optimize;
748 internal bool multiline;
751 internal UndoClass undo;
753 internal Marker caret;
754 internal Marker selection_start;
755 internal Marker selection_end;
756 internal bool selection_visible;
757 internal Marker selection_anchor;
758 internal Marker selection_prev;
759 internal bool selection_end_anchor;
761 internal int viewport_x;
762 internal int viewport_y; // The visible area of the document
763 internal int viewport_width;
764 internal int viewport_height;
766 internal int document_x; // Width of the document
767 internal int document_y; // Height of the document
769 internal Rectangle invalid;
771 internal int crlf_size; // 1 or 2, depending on whether we use \r\n or just \n
773 internal TextBoxBase owner; // Who's owning us?
774 static internal int caret_width = 1;
775 static internal int caret_shift = 1;
776 #endregion // Local Variables
779 internal Document(TextBoxBase owner) {
788 recalc_pending = false;
790 // Tree related stuff
791 sentinel = new Line();
792 sentinel.color = LineColor.Black;
795 last_found = sentinel;
797 // We always have a blank line
798 owner.HandleCreated += new EventHandler(owner_HandleCreated);
799 owner.VisibleChanged += new EventHandler(owner_VisibleChanged);
801 Add(1, "", owner.Font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.ForeColor));
802 Line l = GetLine (1);
805 undo = new UndoClass(this);
807 selection_visible = false;
808 selection_start.line = this.document;
809 selection_start.pos = 0;
810 selection_start.tag = selection_start.line.tags;
811 selection_end.line = this.document;
812 selection_end.pos = 0;
813 selection_end.tag = selection_end.line.tags;
814 selection_anchor.line = this.document;
815 selection_anchor.pos = 0;
816 selection_anchor.tag = selection_anchor.line.tags;
817 caret.line = this.document;
819 caret.tag = caret.line.tags;
826 // Default selection is empty
828 document_id = random.Next();
832 #region Internal Properties
849 internal Line CaretLine {
855 internal int CaretPosition {
861 internal Point Caret {
863 return new Point((int)caret.tag.line.widths[caret.pos] + caret.line.align_shift, caret.line.Y);
867 internal LineTag CaretTag {
877 internal int CRLFSize {
887 internal string PasswordChar {
889 return password_char;
893 password_char = value;
894 if ((password_char.Length != 0) && (password_char[0] != '\0')) {
899 password_cache = new StringBuilder(1024);
900 for (int i = 0; i < 1024; i++) {
901 password_cache.Append(ch);
905 password_cache = null;
910 internal int ViewPortX {
920 internal int Length {
922 return char_count + lines - 1; // Add \n for each line but the last
926 private int CharCount {
934 if (LengthChanged != null) {
935 LengthChanged(this, EventArgs.Empty);
940 ///<summary>Setting NoRecalc to true will prevent the document from being recalculated.
941 ///This ensures that coordinates of added text are predictable after adding the text even with wrapped view</summary>
942 internal bool NoRecalc {
949 if (!no_recalc && recalc_pending) {
950 RecalculateDocument(owner.CreateGraphicsInternal(), recalc_start, recalc_end, recalc_optimize);
951 recalc_pending = false;
956 internal int ViewPortY {
966 internal int ViewPortWidth {
968 return viewport_width;
972 viewport_width = value;
976 internal int ViewPortHeight {
978 return viewport_height;
982 viewport_height = value;
989 return this.document_x;
993 internal int Height {
995 return this.document_y;
999 internal bool SelectionVisible {
1001 return selection_visible;
1005 internal bool Wrap {
1015 #endregion // Internal Properties
1017 #region Private Methods
1019 internal int DumpTree(Line line, bool with_tags) {
1024 Console.Write("Line {0} [# {1}], Y: {2} Text {3}", line.line_no, line.GetHashCode(), line.Y, line.text != null ? line.text.ToString() : "undefined");
1026 if (line.left == sentinel) {
1027 Console.Write(", left = sentinel");
1028 } else if (line.left == null) {
1029 Console.Write(", left = NULL");
1032 if (line.right == sentinel) {
1033 Console.Write(", right = sentinel");
1034 } else if (line.right == null) {
1035 Console.Write(", right = NULL");
1038 Console.WriteLine("");
1048 Console.Write(" Tags: ");
1049 while (tag != null) {
1050 Console.Write("{0} <{1}>-<{2}> ", count++, tag.start, tag.length);
1051 length += tag.length;
1053 if (tag.line != line) {
1054 Console.Write("BAD line link");
1055 throw new Exception("Bad line link in tree");
1059 Console.Write(", ");
1062 if (length > line.text.Length) {
1063 throw new Exception(String.Format("Length of tags more than length of text on line (expected {0} calculated {1})", line.text.Length, length));
1064 } else if (length < line.text.Length) {
1065 throw new Exception(String.Format("Length of tags less than length of text on line (expected {0} calculated {1})", line.text.Length, length));
1067 Console.WriteLine("");
1069 if (line.left != null) {
1070 if (line.left != sentinel) {
1071 total += DumpTree(line.left, with_tags);
1074 if (line != sentinel) {
1075 throw new Exception("Left should not be NULL");
1079 if (line.right != null) {
1080 if (line.right != sentinel) {
1081 total += DumpTree(line.right, with_tags);
1084 if (line != sentinel) {
1085 throw new Exception("Right should not be NULL");
1089 for (int i = 1; i <= this.lines; i++) {
1090 if (GetLine(i) == null) {
1091 throw new Exception(String.Format("Hole in line order, missing {0}", i));
1095 if (line == this.Root) {
1096 if (total < this.lines) {
1097 throw new Exception(String.Format("Not enough nodes in tree, found {0}, expected {1}", total, this.lines));
1098 } else if (total > this.lines) {
1099 throw new Exception(String.Format("Too many nodes in tree, found {0}, expected {1}", total, this.lines));
1106 private void SetSelectionVisible (bool value)
1108 selection_visible = value;
1110 // cursor and selection are enemies, we can't have both in the same room at the same time
1111 if (owner.IsHandleCreated)
1112 XplatUI.CaretVisible (owner.Handle, !selection_visible);
1115 private void DecrementLines(int line_no) {
1119 while (current <= lines) {
1120 GetLine(current).line_no--;
1126 private void IncrementLines(int line_no) {
1129 current = this.lines;
1130 while (current >= line_no) {
1131 GetLine(current).line_no++;
1137 private void RebalanceAfterAdd(Line line1) {
1140 while ((line1 != document) && (line1.parent.color == LineColor.Red)) {
1141 if (line1.parent == line1.parent.parent.left) {
1142 line2 = line1.parent.parent.right;
1144 if ((line2 != null) && (line2.color == LineColor.Red)) {
1145 line1.parent.color = LineColor.Black;
1146 line2.color = LineColor.Black;
1147 line1.parent.parent.color = LineColor.Red;
1148 line1 = line1.parent.parent;
1150 if (line1 == line1.parent.right) {
1151 line1 = line1.parent;
1155 line1.parent.color = LineColor.Black;
1156 line1.parent.parent.color = LineColor.Red;
1158 RotateRight(line1.parent.parent);
1161 line2 = line1.parent.parent.left;
1163 if ((line2 != null) && (line2.color == LineColor.Red)) {
1164 line1.parent.color = LineColor.Black;
1165 line2.color = LineColor.Black;
1166 line1.parent.parent.color = LineColor.Red;
1167 line1 = line1.parent.parent;
1169 if (line1 == line1.parent.left) {
1170 line1 = line1.parent;
1174 line1.parent.color = LineColor.Black;
1175 line1.parent.parent.color = LineColor.Red;
1176 RotateLeft(line1.parent.parent);
1180 document.color = LineColor.Black;
1183 private void RebalanceAfterDelete(Line line1) {
1186 while ((line1 != document) && (line1.color == LineColor.Black)) {
1187 if (line1 == line1.parent.left) {
1188 line2 = line1.parent.right;
1189 if (line2.color == LineColor.Red) {
1190 line2.color = LineColor.Black;
1191 line1.parent.color = LineColor.Red;
1192 RotateLeft(line1.parent);
1193 line2 = line1.parent.right;
1195 if ((line2.left.color == LineColor.Black) && (line2.right.color == LineColor.Black)) {
1196 line2.color = LineColor.Red;
1197 line1 = line1.parent;
1199 if (line2.right.color == LineColor.Black) {
1200 line2.left.color = LineColor.Black;
1201 line2.color = LineColor.Red;
1203 line2 = line1.parent.right;
1205 line2.color = line1.parent.color;
1206 line1.parent.color = LineColor.Black;
1207 line2.right.color = LineColor.Black;
1208 RotateLeft(line1.parent);
1212 line2 = line1.parent.left;
1213 if (line2.color == LineColor.Red) {
1214 line2.color = LineColor.Black;
1215 line1.parent.color = LineColor.Red;
1216 RotateRight(line1.parent);
1217 line2 = line1.parent.left;
1219 if ((line2.right.color == LineColor.Black) && (line2.left.color == LineColor.Black)) {
1220 line2.color = LineColor.Red;
1221 line1 = line1.parent;
1223 if (line2.left.color == LineColor.Black) {
1224 line2.right.color = LineColor.Black;
1225 line2.color = LineColor.Red;
1227 line2 = line1.parent.left;
1229 line2.color = line1.parent.color;
1230 line1.parent.color = LineColor.Black;
1231 line2.left.color = LineColor.Black;
1232 RotateRight(line1.parent);
1237 line1.color = LineColor.Black;
1240 private void RotateLeft(Line line1) {
1241 Line line2 = line1.right;
1243 line1.right = line2.left;
1245 if (line2.left != sentinel) {
1246 line2.left.parent = line1;
1249 if (line2 != sentinel) {
1250 line2.parent = line1.parent;
1253 if (line1.parent != null) {
1254 if (line1 == line1.parent.left) {
1255 line1.parent.left = line2;
1257 line1.parent.right = line2;
1264 if (line1 != sentinel) {
1265 line1.parent = line2;
1269 private void RotateRight(Line line1) {
1270 Line line2 = line1.left;
1272 line1.left = line2.right;
1274 if (line2.right != sentinel) {
1275 line2.right.parent = line1;
1278 if (line2 != sentinel) {
1279 line2.parent = line1.parent;
1282 if (line1.parent != null) {
1283 if (line1 == line1.parent.right) {
1284 line1.parent.right = line2;
1286 line1.parent.left = line2;
1292 line2.right = line1;
1293 if (line1 != sentinel) {
1294 line1.parent = line2;
1299 internal void UpdateView(Line line, int pos) {
1300 if (!owner.IsHandleCreated) {
1305 recalc_start = line.line_no;
1306 recalc_end = line.line_no;
1307 recalc_optimize = true;
1308 recalc_pending = true;
1312 // Optimize invalidation based on Line alignment
1313 if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no, true)) {
1314 // Lineheight changed, invalidate the rest of the document
1315 if ((line.Y - viewport_y) >=0 ) {
1316 // We formatted something that's in view, only draw parts of the screen
1317 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, owner.Height - line.Y - viewport_y));
1319 // The tag was above the visible area, draw everything
1323 switch(line.alignment) {
1324 case HorizontalAlignment.Left: {
1325 owner.Invalidate(new Rectangle((int)line.widths[pos] - viewport_x - 1, line.Y - viewport_y, viewport_width, line.height + 1));
1329 case HorizontalAlignment.Center: {
1330 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, line.height + 1));
1334 case HorizontalAlignment.Right: {
1335 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, (int)line.widths[pos + 1] - viewport_x + line.align_shift, line.height + 1));
1343 // Update display from line, down line_count lines; pos is unused, but required for the signature
1344 internal void UpdateView(Line line, int line_count, int pos) {
1345 if (!owner.IsHandleCreated) {
1350 recalc_start = line.line_no;
1351 recalc_end = line.line_no + line_count - 1;
1352 recalc_optimize = true;
1353 recalc_pending = true;
1357 if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no + line_count - 1, true)) {
1358 // Lineheight changed, invalidate the rest of the document
1359 if ((line.Y - viewport_y) >=0 ) {
1360 // We formatted something that's in view, only draw parts of the screen
1361 //blah Console.WriteLine("TextControl.cs(981) Invalidate called in UpdateView(line, line_count, pos)");
1362 owner.Invalidate(new Rectangle(0, line.Y - viewport_y, viewport_width, owner.Height - line.Y - viewport_y));
1364 // The tag was above the visible area, draw everything
1365 //blah Console.WriteLine("TextControl.cs(985) Invalidate called in UpdateView(line, line_count, pos)");
1371 end_line = GetLine(line.line_no + line_count -1);
1372 if (end_line == null) {
1376 //blah Console.WriteLine("TextControl.cs(996) Invalidate called in UpdateView(line, line_count, pos)");
1377 owner.Invalidate(new Rectangle(0 - viewport_x, line.Y - viewport_y, (int)line.widths[line.text.Length], end_line.Y + end_line.height));
1380 #endregion // Private Methods
1382 #region Internal Methods
1383 // Clear the document and reset state
1384 internal void Empty() {
1386 document = sentinel;
1387 last_found = sentinel;
1390 // We always have a blank line
1391 Add(1, "", owner.Font, ThemeEngine.Current.ResPool.GetSolidBrush(owner.ForeColor));
1392 Line l = GetLine (1);
1393 l.soft_break = true;
1395 this.RecalculateDocument(owner.CreateGraphicsInternal());
1396 PositionCaret(0, 0);
1398 SetSelectionVisible (false);
1400 selection_start.line = this.document;
1401 selection_start.pos = 0;
1402 selection_start.tag = selection_start.line.tags;
1403 selection_end.line = this.document;
1404 selection_end.pos = 0;
1405 selection_end.tag = selection_end.line.tags;
1415 internal void PositionCaret(Line line, int pos) {
1416 if (owner.IsHandleCreated) {
1417 undo.RecordCursor();
1420 caret.tag = line.FindTag(pos);
1423 caret.height = caret.tag.height;
1425 if (owner.IsHandleCreated) {
1426 if (owner.Focused) {
1427 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1430 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1436 internal void PositionCaret(int x, int y) {
1437 if (!owner.IsHandleCreated) {
1441 undo.RecordCursor();
1443 caret.tag = FindCursor(x, y, out caret.pos);
1444 caret.line = caret.tag.line;
1445 caret.height = caret.tag.height;
1447 if (owner.Focused) {
1448 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1451 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1454 internal void CaretHasFocus() {
1455 if ((caret.tag != null) && owner.IsHandleCreated) {
1456 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1457 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1463 internal void CaretLostFocus() {
1464 if (!owner.IsHandleCreated) {
1467 XplatUI.DestroyCaret(owner.Handle);
1470 internal void AlignCaret() {
1471 if (!owner.IsHandleCreated) {
1475 undo.RecordCursor();
1477 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1478 caret.height = caret.tag.height;
1480 if (owner.Focused) {
1481 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1482 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1486 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1489 internal void UpdateCaret() {
1490 if (!owner.IsHandleCreated || caret.tag == null) {
1494 undo.RecordCursor();
1496 if (caret.tag.height != caret.height) {
1497 caret.height = caret.tag.height;
1498 if (owner.Focused) {
1499 XplatUI.CreateCaret(owner.Handle, caret_width, caret.height);
1503 XplatUI.SetCaretPos(owner.Handle, (int)caret.tag.line.widths[caret.pos] + caret.line.align_shift - viewport_x, caret.line.Y + caret.tag.shift - viewport_y + caret_shift);
1507 if (CaretMoved != null) CaretMoved(this, EventArgs.Empty);
1510 internal void DisplayCaret() {
1511 if (!owner.IsHandleCreated) {
1515 if (owner.Focused && !selection_visible) {
1516 XplatUI.CaretVisible(owner.Handle, true);
1520 internal void HideCaret() {
1521 if (!owner.IsHandleCreated) {
1525 if (owner.Focused) {
1526 XplatUI.CaretVisible(owner.Handle, false);
1530 internal void MoveCaret(CaretDirection direction) {
1531 // FIXME should we use IsWordSeparator to detect whitespace, instead
1532 // of looking for actual spaces in the Word move cases?
1534 bool nowrap = false;
1536 case CaretDirection.CharForwardNoWrap:
1538 goto case CaretDirection.CharForward;
1539 case CaretDirection.CharForward: {
1541 if (caret.pos > caret.line.text.Length) {
1542 if (multiline && !nowrap) {
1543 // Go into next line
1544 if (caret.line.line_no < this.lines) {
1545 caret.line = GetLine(caret.line.line_no+1);
1547 caret.tag = caret.line.tags;
1552 // Single line; we stay where we are
1556 if ((caret.tag.start - 1 + caret.tag.length) < caret.pos) {
1557 caret.tag = caret.tag.next;
1564 case CaretDirection.CharBackNoWrap:
1566 goto case CaretDirection.CharBack;
1567 case CaretDirection.CharBack: {
1568 if (caret.pos > 0) {
1569 // caret.pos--; // folded into the if below
1570 if (--caret.pos > 0) {
1571 if (caret.tag.start > caret.pos) {
1572 caret.tag = caret.tag.previous;
1576 if (caret.line.line_no > 1 && !nowrap) {
1577 caret.line = GetLine(caret.line.line_no - 1);
1578 caret.pos = caret.line.text.Length;
1579 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1586 case CaretDirection.WordForward: {
1589 len = caret.line.text.Length;
1590 if (caret.pos < len) {
1591 while ((caret.pos < len) && (caret.line.text[caret.pos] != ' ')) {
1594 if (caret.pos < len) {
1595 // Skip any whitespace
1596 while ((caret.pos < len) && (caret.line.text[caret.pos] == ' ')) {
1600 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1602 if (caret.line.line_no < this.lines) {
1603 caret.line = GetLine(caret.line.line_no + 1);
1605 caret.tag = caret.line.tags;
1612 case CaretDirection.WordBack: {
1613 if (caret.pos > 0) {
1616 while ((caret.pos > 0) && (caret.line.text[caret.pos] == ' ')) {
1620 while ((caret.pos > 0) && (caret.line.text[caret.pos] != ' ')) {
1624 if (caret.line.text.ToString(caret.pos, 1) == " ") {
1625 if (caret.pos != 0) {
1628 caret.line = GetLine(caret.line.line_no - 1);
1629 caret.pos = caret.line.text.Length;
1632 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1634 if (caret.line.line_no > 1) {
1635 caret.line = GetLine(caret.line.line_no - 1);
1636 caret.pos = caret.line.text.Length;
1637 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1644 case CaretDirection.LineUp: {
1645 if (caret.line.line_no > 1) {
1648 pixel = (int)caret.line.widths[caret.pos];
1649 PositionCaret(pixel, GetLine(caret.line.line_no - 1).Y);
1656 case CaretDirection.LineDown: {
1657 if (caret.line.line_no < lines) {
1660 pixel = (int)caret.line.widths[caret.pos];
1661 PositionCaret(pixel, GetLine(caret.line.line_no + 1).Y);
1668 case CaretDirection.Home: {
1669 if (caret.pos > 0) {
1671 caret.tag = caret.line.tags;
1677 case CaretDirection.End: {
1678 if (caret.pos < caret.line.text.Length) {
1679 caret.pos = caret.line.text.Length;
1680 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1686 case CaretDirection.PgUp: {
1688 int new_y, y_offset;
1690 if (viewport_y == 0) {
1692 // This should probably be handled elsewhere
1693 if (!(owner is RichTextBox)) {
1694 // Page down doesn't do anything in a regular TextBox
1695 // if the bottom of the document
1696 // is already visible, the page and the caret stay still
1700 // We're just placing the caret at the end of the document, no scrolling needed
1701 owner.vscroll.Value = 0;
1702 Line line = GetLine (1);
1703 PositionCaret (line, 0);
1706 y_offset = caret.line.Y - viewport_y;
1707 new_y = caret.line.Y - viewport_height;
1709 owner.vscroll.Value = Math.Max (new_y, 0);
1710 PositionCaret ((int)caret.line.widths[caret.pos], y_offset + viewport_y);
1714 case CaretDirection.PgDn: {
1715 int new_y, y_offset;
1717 if ((viewport_y + viewport_height) > document_y) {
1719 // This should probably be handled elsewhere
1720 if (!(owner is RichTextBox)) {
1721 // Page up doesn't do anything in a regular TextBox
1722 // if the bottom of the document
1723 // is already visible, the page and the caret stay still
1727 // We're just placing the caret at the end of the document, no scrolling needed
1728 owner.vscroll.Value = owner.vscroll.Maximum - viewport_height + 1;
1729 Line line = GetLine (lines);
1730 PositionCaret (line, line.Text.Length);
1733 y_offset = caret.line.Y - viewport_y;
1734 new_y = caret.line.Y + viewport_height;
1736 owner.vscroll.Value = Math.Min (new_y, owner.vscroll.Maximum - viewport_height + 1);
1737 PositionCaret ((int)caret.line.widths[caret.pos], y_offset + viewport_y);
1742 case CaretDirection.CtrlPgUp: {
1743 PositionCaret(0, viewport_y);
1748 case CaretDirection.CtrlPgDn: {
1753 tag = FindTag(0, viewport_y + viewport_height, out index, false);
1754 if (tag.line.line_no > 1) {
1755 line = GetLine(tag.line.line_no - 1);
1759 PositionCaret(line, line.Text.Length);
1764 case CaretDirection.CtrlHome: {
1765 caret.line = GetLine(1);
1767 caret.tag = caret.line.tags;
1773 case CaretDirection.CtrlEnd: {
1774 caret.line = GetLine(lines);
1775 caret.pos = caret.line.text.Length;
1776 caret.tag = LineTag.FindTag(caret.line, caret.pos);
1782 case CaretDirection.SelectionStart: {
1783 caret.line = selection_start.line;
1784 caret.pos = selection_start.pos;
1785 caret.tag = selection_start.tag;
1791 case CaretDirection.SelectionEnd: {
1792 caret.line = selection_end.line;
1793 caret.pos = selection_end.pos;
1794 caret.tag = selection_end.tag;
1802 // Draw the document
1803 internal void Draw(Graphics g, Rectangle clip) {
1804 Line line; // Current line being drawn
1805 LineTag tag; // Current tag being drawn
1806 int start; // First line to draw
1807 int end; // Last line to draw
1808 StringBuilder text; // String representing the current line
1814 // First, figure out from what line to what line we need to draw
1815 start = GetLineByPixel(clip.Top + viewport_y, false).line_no;
1816 end = GetLineByPixel(clip.Bottom + viewport_y, false).line_no;
1817 //Console.WriteLine("Starting drawing at line {0}, ending at line {1} (clip-bottom:{2})", start, end, clip.Bottom);
1819 // Now draw our elements; try to only draw those that are visible
1823 DateTime n = DateTime.Now;
1824 Console.WriteLine("Started drawing: {0}s {1}ms", n.Second, n.Millisecond);
1827 disabled = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorGrayText);
1828 hilight = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorHighlight);
1829 hilight_text = ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorHighlightText);
1831 while (line_no <= end) {
1832 line = GetLine(line_no);
1834 if (owner.backcolor_set || (owner.Enabled && !owner.read_only)) {
1835 g.FillRectangle(ThemeEngine.Current.ResPool.GetSolidBrush(owner.BackColor), new Rectangle(clip.Left, line.Y - viewport_y, clip.Width, line.Y - viewport_y + line.Height));
1837 g.FillRectangle(ThemeEngine.Current.ResPool.GetSolidBrush(ThemeEngine.Current.ColorControl), new Rectangle(clip.Left, line.Y - viewport_y, clip.Width, line.Y - viewport_y + line.Height));
1846 // This fails if there's a password > 1024 chars...
1847 text = this.password_cache;
1849 while (tag != null) {
1850 if (tag.length == 0) {
1855 if (((tag.X + tag.width) > (clip.Left - viewport_x)) || (tag.X < (clip.Right - viewport_x))) {
1856 // Check for selection
1857 if ((!selection_visible) || (!owner.ShowSelection) || (line_no < selection_start.line.line_no) || (line_no > selection_end.line.line_no)) {
1858 // regular drawing, no selection to deal with
1859 //g.DrawString(s.Substring(tag.start-1, tag.length), tag.font, tag.color, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1860 if (owner.is_enabled) {
1861 g.DrawString(text.ToString(tag.start-1, tag.length), tag.font, tag.color, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1866 a = ((SolidBrush)tag.color).Color;
1867 b = ThemeEngine.Current.ColorWindowText;
1869 if ((a.R == b.R) && (a.G == b.G) && (a.B == b.B)) {
1870 g.DrawString(text.ToString(tag.start-1, tag.length), tag.font, disabled, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1872 g.DrawString(text.ToString(tag.start-1, tag.length), tag.font, tag.color, tag.X + line.align_shift - viewport_x, line.Y + tag.shift - viewport_y, StringFormat.GenericTypographic);
1876 // we might have to draw our selection
1877 if ((line_no != selection_start.line.line_no) && (line_no != selection_end.line.line_no)) {
1878 // Special case, whole line is selected, draw this tag selected
1881 tag.X + line.align_shift - viewport_x, // X
1882 line.Y + tag.shift - viewport_y, // Y
1883 line.widths[tag.start + tag.length - 1], // width
1884 tag.height // Height
1888 //s.Substring(tag.start-1, tag.length), // String
1889 text.ToString(tag.start-1, tag.length), // String
1891 hilight_text, // Brush
1892 tag.X + line.align_shift - viewport_x, // X
1893 line.Y + tag.shift - viewport_y, // Y
1894 StringFormat.GenericTypographic);
1902 // One or more, but not all tags on the line are selected
1903 if ((selection_start.tag == tag) && (selection_end.tag == tag)) {
1904 // Single tag selected, draw "normalSELECTEDnormal"
1906 // First, the regular part
1908 //s.Substring(tag.start - 1, selection_start.pos - tag.start + 1), // String
1909 text.ToString(tag.start - 1, selection_start.pos - tag.start + 1), // String
1912 tag.X + line.align_shift - viewport_x, // X
1913 line.Y + tag.shift - viewport_y, // Y
1914 StringFormat.GenericTypographic);
1916 // Now the highlight
1919 line.widths[selection_start.pos] + line.align_shift - viewport_x, // X
1920 line.Y + tag.shift - viewport_y, // Y
1921 line.widths[selection_end.pos] - line.widths[selection_start.pos], // Width
1922 tag.height); // Height
1925 //s.Substring(selection_start.pos, selection_end.pos - selection_start.pos), // String
1926 text.ToString(selection_start.pos, selection_end.pos - selection_start.pos), // String
1928 hilight_text, // Brush
1929 line.widths[selection_start.pos] + line.align_shift - viewport_x, // X
1930 line.Y + tag.shift - viewport_y, // Y
1931 StringFormat.GenericTypographic);
1933 // And back to the regular
1935 //s.Substring(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1936 text.ToString(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1939 line.widths[selection_end.pos] + line.align_shift - viewport_x, // X
1940 line.Y + tag.shift - viewport_y, // Y
1941 StringFormat.GenericTypographic);
1943 } else if (selection_start.tag == tag) {
1946 // The highlighted part
1949 line.widths[selection_start.pos] + line.align_shift - viewport_x,
1950 line.Y + tag.shift - viewport_y,
1951 line.widths[tag.start + tag.length - 1] - line.widths[selection_start.pos],
1955 //s.Substring(selection_start.pos, tag.start + tag.length - selection_start.pos - 1), // String
1956 text.ToString(selection_start.pos, tag.start + tag.length - selection_start.pos - 1), // String
1958 hilight_text, // Brush
1959 line.widths[selection_start.pos] + line.align_shift - viewport_x, // X
1960 line.Y + tag.shift - viewport_y, // Y
1961 StringFormat.GenericTypographic);
1965 //s.Substring(tag.start - 1, selection_start.pos - tag.start + 1), // String
1966 text.ToString(tag.start - 1, selection_start.pos - tag.start + 1), // String
1969 tag.X + line.align_shift - viewport_x, // X
1970 line.Y + tag.shift - viewport_y, // Y
1971 StringFormat.GenericTypographic);
1972 } else if (selection_end.tag == tag) {
1975 // The highlighted part
1978 tag.X + line.align_shift - viewport_x,
1979 line.Y + tag.shift - viewport_y,
1980 line.widths[selection_end.pos] - line.widths[tag.start - 1],
1984 //s.Substring(tag.start - 1, selection_end.pos - tag.start + 1), // String
1985 text.ToString(tag.start - 1, selection_end.pos - tag.start + 1), // String
1987 hilight_text, // Brush
1988 tag.X + line.align_shift - viewport_x, // X
1989 line.Y + tag.shift - viewport_y, // Y
1990 StringFormat.GenericTypographic);
1994 //s.Substring(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1995 text.ToString(selection_end.pos, tag.start + tag.length - selection_end.pos - 1), // String
1998 line.widths[selection_end.pos] + line.align_shift - viewport_x, // X
1999 line.Y + tag.shift - viewport_y, // Y
2000 StringFormat.GenericTypographic);
2002 // no partially selected tags here, simple checks...
2003 if (selection_start.line == line) {
2007 begin = tag.start - 1;
2008 stop = tag.start + tag.length - 1;
2009 if (selection_end.line == line) {
2010 if ((begin >= selection_start.pos) && (stop < selection_end.pos)) {
2014 if (stop > selection_start.pos) {
2018 } else if (selection_end.line == line) {
2019 if ((tag.start - 1) < selection_end.pos) {
2029 tag.X + line.align_shift - viewport_x,
2030 line.Y + tag.shift - viewport_y,
2031 line.widths[tag.start + tag.length - 1] - line.widths[tag.start - 1],
2035 //s.Substring(tag.start-1, tag.length), // String
2036 text.ToString(tag.start-1, tag.length), // String
2038 hilight_text, // Brush
2039 tag.X + line.align_shift - viewport_x, // X
2040 line.Y + tag.shift - viewport_y, // Y
2041 StringFormat.GenericTypographic);
2044 //s.Substring(tag.start-1, tag.length), // String
2045 text.ToString(tag.start-1, tag.length), // String
2048 tag.X + line.align_shift - viewport_x, // X
2049 line.Y + tag.shift - viewport_y, // Y
2050 StringFormat.GenericTypographic);
2065 Console.WriteLine("Finished drawing: {0}s {1}ms", n.Second, n.Millisecond);
2070 internal void Insert(Line line, int pos, string s) {
2071 Insert(line, null, pos, false, s);
2074 // Insert multi-line text at the given position; use formatting at insertion point for inserted text
2075 internal void Insert(Line line, LineTag tag, int pos, bool update_caret, string s) {
2082 // The formatting at the insertion point is used for the inserted text
2084 tag = LineTag.FindTag(line, pos);
2087 base_line = line.line_no;
2089 ins = s.Split(new char[] {'\n'});
2091 for (int j = 0; j < ins.Length; j++) {
2092 if (ins[j].EndsWith("\r")) {
2093 ins[j] = ins[j].Substring(0, ins[j].Length - 1);
2097 insert_lines = ins.Length;
2098 old_line_count = lines;
2100 // Bump the text at insertion point a line down if we're inserting more than one line
2101 if (insert_lines > 1) {
2103 // Remainder of start line is now in base_line + 1
2106 // Insert the first line
2107 InsertString(tag, pos, ins[0]);
2109 if (insert_lines > 1) {
2110 for (i = 1; i < insert_lines; i++) {
2111 Add(base_line + i, ins[i], line.alignment, tag.font, tag.color);
2113 if (!s.EndsWith("\n\n")) {
2114 this.Combine(base_line + (lines - old_line_count) - 1, base_line + lines - old_line_count);
2118 UpdateView(line, lines - old_line_count + 1, pos);
2121 // Move caret to the end of the inserted text
2122 if (insert_lines > 1) {
2123 Line l = GetLine (line.line_no + lines - old_line_count);
2124 PositionCaret(l, l.text.Length);
2126 PositionCaret(line, pos + ins[0].Length);
2132 // Inserts a character at the given position
2133 internal void InsertString(Line line, int pos, string s) {
2134 InsertString(line.FindTag(pos), pos, s);
2137 // Inserts a string at the given position
2138 internal void InsertString(LineTag tag, int pos, string s) {
2147 line.text.Insert(pos, s);
2150 // TODO: sometimes getting a null tag here when pasting ???
2152 while (tag != null) {
2159 UpdateView(line, pos);
2162 // Inserts a string at the caret position
2163 internal void InsertStringAtCaret(string s, bool move_caret) {
2171 caret.line.text.Insert(caret.pos, s);
2172 caret.tag.length += len;
2174 if (caret.tag.next != null) {
2175 tag = caret.tag.next;
2176 while (tag != null) {
2181 caret.line.Grow(len);
2182 caret.line.recalc = true;
2184 UpdateView(caret.line, caret.pos);
2193 // Inserts a character at the given position
2194 internal void InsertChar(Line line, int pos, char ch) {
2195 InsertChar(line.FindTag(pos), pos, ch);
2198 // Inserts a character at the given position
2199 internal void InsertChar(LineTag tag, int pos, char ch) {
2205 line.text.Insert(pos, ch);
2209 while (tag != null) {
2216 UpdateView(line, pos);
2219 // Inserts a character at the current caret position
2220 internal void InsertCharAtCaret(char ch, bool move_caret) {
2225 caret.line.text.Insert(caret.pos, ch);
2228 if (caret.tag.next != null) {
2229 tag = caret.tag.next;
2230 while (tag != null) {
2236 caret.line.recalc = true;
2238 UpdateView(caret.line, caret.pos);
2242 SetSelectionToCaret(true);
2246 // Deletes n characters at the given position; it will not delete past line limits
2248 internal void DeleteChars(LineTag tag, int pos, int count) {
2257 if (pos == line.text.Length) {
2261 line.text.Remove(pos, count);
2263 // Make sure the tag points to the right spot
2264 while ((tag != null) && (tag.start + tag.length - 1) <= pos) {
2272 // Check if we're crossing tag boundaries
2273 if ((pos + count) > (tag.start + tag.length - 1)) {
2276 // We have to delete cross tag boundaries
2280 left -= tag.start + tag.length - pos - 1;
2281 tag.length -= tag.start + tag.length - pos - 1;
2284 while ((tag != null) && (left > 0)) {
2285 tag.start -= count - left;
2286 if (tag.length > left) {
2297 // We got off easy, same tag
2299 tag.length -= count;
2301 if (tag.length == 0) {
2306 // Delete empty orphaned tags at the end
2308 while (walk != null && walk.next != null && walk.next.length == 0) {
2310 walk.next = walk.next.next;
2311 if (walk.next != null)
2312 walk.next.previous = t;
2316 // Adjust the start point of any tags following
2319 while (tag != null) {
2327 line.Streamline(lines);
2330 UpdateView(line, pos);
2333 // Deletes a character at or after the given position (depending on forward); it will not delete past line limits
2334 internal void DeleteChar(LineTag tag, int pos, bool forward) {
2343 if ((pos == 0 && forward == false) || (pos == line.text.Length && forward == true)) {
2349 line.text.Remove(pos, 1);
2351 while ((tag != null) && (tag.start + tag.length - 1) <= pos) {
2361 if (tag.length == 0) {
2366 line.text.Remove(pos, 1);
2367 if (pos >= (tag.start - 1)) {
2369 if (tag.length == 0) {
2372 } else if (tag.previous != null) {
2373 tag.previous.length--;
2374 if (tag.previous.length == 0) {
2380 // Delete empty orphaned tags at the end
2382 while (walk != null && walk.next != null && walk.next.length == 0) {
2384 walk.next = walk.next.next;
2385 if (walk.next != null)
2386 walk.next.previous = t;
2391 while (tag != null) {
2397 line.Streamline(lines);
2400 UpdateView(line, pos);
2403 // Combine two lines
2404 internal void Combine(int FirstLine, int SecondLine) {
2405 Combine(GetLine(FirstLine), GetLine(SecondLine));
2408 internal void Combine(Line first, Line second) {
2412 // Combine the two tag chains into one
2415 // Maintain the line ending style
2416 first.soft_break = second.soft_break;
2418 while (last.next != null) {
2422 last.next = second.tags;
2423 last.next.previous = last;
2425 shift = last.start + last.length - 1;
2427 // Fix up references within the chain
2429 while (last != null) {
2431 last.start += shift;
2435 // Combine both lines' strings
2436 first.text.Insert(first.text.Length, second.text.ToString());
2437 first.Grow(first.text.Length);
2439 // Remove the reference to our (now combined) tags from the doomed line
2443 DecrementLines(first.line_no + 2); // first.line_no + 1 will be deleted, so we need to start renumbering one later
2446 first.recalc = true;
2447 first.height = 0; // This forces RecalcDocument/UpdateView to redraw from this line on
2448 first.Streamline(lines);
2450 // Update Caret, Selection, etc
2451 if (caret.line == second) {
2452 caret.Combine(first, shift);
2454 if (selection_anchor.line == second) {
2455 selection_anchor.Combine(first, shift);
2457 if (selection_start.line == second) {
2458 selection_start.Combine(first, shift);
2460 if (selection_end.line == second) {
2461 selection_end.Combine(first, shift);
2468 check_first = GetLine(first.line_no);
2469 check_second = GetLine(check_first.line_no + 1);
2471 Console.WriteLine("Pre-delete: Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
2474 this.Delete(second);
2477 check_first = GetLine(first.line_no);
2478 check_second = GetLine(check_first.line_no + 1);
2480 Console.WriteLine("Post-delete Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y);
2484 // Split the line at the position into two
2485 internal void Split(int LineNo, int pos) {
2489 line = GetLine(LineNo);
2490 tag = LineTag.FindTag(line, pos);
2491 Split(line, tag, pos, false);
2494 internal void Split(Line line, int pos) {
2497 tag = LineTag.FindTag(line, pos);
2498 Split(line, tag, pos, false);
2501 ///<summary>Split line at given tag and position into two lines</summary>
2502 ///<param name="soft">True if the split should be marked as 'soft', indicating that it can be contracted
2503 ///if more space becomes available on previous line</param>
2504 internal void Split(Line line, LineTag tag, int pos, bool soft) {
2508 bool move_sel_start;
2512 move_sel_start = false;
2513 move_sel_end = false;
2515 // Adjust selection and cursors
2516 if (soft && (caret.line == line) && (caret.pos >= pos)) {
2519 if (selection_start.line == line && selection_start.pos > pos) {
2520 move_sel_start = true;
2523 if (selection_end.line == line && selection_end.pos > pos) {
2524 move_sel_end = true;
2527 // cover the easy case first
2528 if (pos == line.text.Length) {
2529 Add(line.line_no + 1, "", line.alignment, tag.font, tag.color);
2531 new_line = GetLine(line.line_no + 1);
2535 caret.line = new_line;
2536 caret.line.soft_break = true;
2537 caret.tag = new_line.tags;
2540 new_line.soft_break = true;
2544 if (move_sel_start) {
2545 selection_start.line = new_line;
2546 selection_start.pos = 0;
2547 selection_start.tag = new_line.tags;
2551 selection_end.line = new_line;
2552 selection_end.pos = 0;
2553 selection_end.tag = new_line.tags;
2558 // We need to move the rest of the text into the new line
2559 Add(line.line_no + 1, line.text.ToString(pos, line.text.Length - pos), line.alignment, tag.font, tag.color);
2561 // Now transfer our tags from this line to the next
2562 new_line = GetLine(line.line_no + 1);
2564 new_line.recalc = true;
2566 if ((tag.start - 1) == pos) {
2569 // We can simply break the chain and move the tag into the next line
2570 if (tag == line.tags) {
2571 new_tag = new LineTag(line, 1, 0);
2572 new_tag.font = tag.font;
2573 new_tag.color = tag.color;
2574 line.tags = new_tag;
2577 if (tag.previous != null) {
2578 tag.previous.next = null;
2580 new_line.tags = tag;
2581 tag.previous = null;
2582 tag.line = new_line;
2584 // Walk the list and correct the start location of the tags we just bumped into the next line
2585 shift = tag.start - 1;
2588 while (new_tag != null) {
2589 new_tag.start -= shift;
2590 new_tag.line = new_line;
2591 new_tag = new_tag.next;
2596 new_tag = new LineTag(new_line, 1, tag.start - 1 + tag.length - pos);
2597 new_tag.next = tag.next;
2598 new_tag.font = tag.font;
2599 new_tag.color = tag.color;
2600 new_line.tags = new_tag;
2601 if (new_tag.next != null) {
2602 new_tag.next.previous = new_tag;
2605 tag.length = pos - tag.start + 1;
2608 new_tag = new_tag.next;
2609 while (new_tag != null) {
2610 new_tag.start -= shift;
2611 new_tag.line = new_line;
2612 new_tag = new_tag.next;
2619 caret.line = new_line;
2620 caret.pos = caret.pos - pos;
2621 caret.tag = caret.line.FindTag(caret.pos);
2623 new_line.soft_break = true;
2626 if (move_sel_start) {
2627 selection_start.line = new_line;
2628 selection_start.pos = selection_start.pos - pos;
2629 selection_start.tag = new_line.FindTag(selection_start.pos);
2633 selection_end.line = new_line;
2634 selection_end.pos = selection_end.pos - pos;
2635 selection_end.tag = new_line.FindTag(selection_end.pos);
2638 CharCount -= line.text.Length - pos;
2639 line.text.Remove(pos, line.text.Length - pos);
2642 // Adds a line of text, with given font.
2643 // Bumps any line at that line number that already exists down
2644 internal void Add(int LineNo, string Text, Font font, Brush color) {
2645 Add(LineNo, Text, HorizontalAlignment.Left, font, color);
2648 internal void Add(int LineNo, string Text, HorizontalAlignment align, Font font, Brush color) {
2653 CharCount += Text.Length;
2655 if (LineNo<1 || Text == null) {
2657 throw new ArgumentNullException("LineNo", "Line numbers must be positive");
2659 throw new ArgumentNullException("Text", "Cannot insert NULL line");
2663 add = new Line(LineNo, Text, align, font, color);
2666 while (line != sentinel) {
2668 line_no = line.line_no;
2670 if (LineNo > line_no) {
2672 } else if (LineNo < line_no) {
2675 // Bump existing line numbers; walk all nodes to the right of this one and increment line_no
2676 IncrementLines(line.line_no);
2681 add.left = sentinel;
2682 add.right = sentinel;
2684 if (add.parent != null) {
2685 if (LineNo > add.parent.line_no) {
2686 add.parent.right = add;
2688 add.parent.left = add;
2695 RebalanceAfterAdd(add);
2700 internal virtual void Clear() {
2703 document = sentinel;
2706 public virtual object Clone() {
2709 clone = new Document(null);
2711 clone.lines = this.lines;
2712 clone.document = (Line)document.Clone();
2717 internal void Delete(int LineNo) {
2724 line = GetLine(LineNo);
2726 CharCount -= line.text.Length;
2728 DecrementLines(LineNo + 1);
2732 internal void Delete(Line line1) {
2733 Line line2;// = new Line();
2736 if ((line1.left == sentinel) || (line1.right == sentinel)) {
2739 line3 = line1.right;
2740 while (line3.left != sentinel) {
2745 if (line3.left != sentinel) {
2748 line2 = line3.right;
2751 line2.parent = line3.parent;
2752 if (line3.parent != null) {
2753 if(line3 == line3.parent.left) {
2754 line3.parent.left = line2;
2756 line3.parent.right = line2;
2762 if (line3 != line1) {
2765 if (selection_start.line == line3) {
2766 selection_start.line = line1;
2769 if (selection_end.line == line3) {
2770 selection_end.line = line1;
2773 if (selection_anchor.line == line3) {
2774 selection_anchor.line = line1;
2777 if (caret.line == line3) {
2782 line1.alignment = line3.alignment;
2783 line1.ascent = line3.ascent;
2784 line1.hanging_indent = line3.hanging_indent;
2785 line1.height = line3.height;
2786 line1.indent = line3.indent;
2787 line1.line_no = line3.line_no;
2788 line1.recalc = line3.recalc;
2789 line1.right_indent = line3.right_indent;
2790 line1.soft_break = line3.soft_break;
2791 line1.space = line3.space;
2792 line1.tags = line3.tags;
2793 line1.text = line3.text;
2794 line1.widths = line3.widths;
2798 while (tag != null) {
2804 if (line3.color == LineColor.Black)
2805 RebalanceAfterDelete(line2);
2809 last_found = sentinel;
2812 // Invalidate a section of the document to trigger redraw
2813 internal void Invalidate(Line start, int start_pos, Line end, int end_pos) {
2819 if ((start == end) && (start_pos == end_pos)) {
2823 if (end_pos == -1) {
2824 end_pos = end.text.Length;
2827 // figure out what's before what so the logic below is straightforward
2828 if (start.line_no < end.line_no) {
2834 } else if (start.line_no > end.line_no) {
2841 if (start_pos < end_pos) {
2856 Console.WriteLine("Invaliding from {0}:{1} to {2}:{3}", l1.line_no, p1, l2.line_no, p2);
2861 (int)l1.widths[p1] + l1.align_shift - viewport_x,
2863 (int)l2.widths[p2] - (int)l1.widths[p1] + 1,
2871 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);
2874 // Three invalidates:
2875 // First line from start
2876 owner.Invalidate(new Rectangle((int)l1.widths[p1] + l1.align_shift - viewport_x, l1.Y - viewport_y, viewport_width, l1.height));
2879 if ((l1.line_no + 1) < l2.line_no) {
2882 y = GetLine(l1.line_no + 1).Y;
2883 owner.Invalidate(new Rectangle(0, y - viewport_y, viewport_width, GetLine(l2.line_no).Y - y - viewport_y));
2886 Console.WriteLine("Invaliding from {0}:{1} to {2}:{3} Middle => x={4}, y={5}, {6}x{7}", l1.line_no, p1, l2.line_no, p2, 0, y - viewport_y, viewport_width, GetLine(l2.line_no).Y - y - viewport_y);
2891 owner.Invalidate(new Rectangle((int)l2.widths[0] + l2.align_shift - viewport_x, l2.Y - viewport_y, (int)l2.widths[p2] + 1, l2.height));
2893 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);
2897 /// <summary>Select text around caret</summary>
2898 internal void ExpandSelection(CaretSelection mode, bool to_caret) {
2900 // We're expanding the selection to the caret position
2902 case CaretSelection.Line: {
2903 // Invalidate the selection delta
2904 if (caret > selection_prev) {
2905 Invalidate(selection_prev.line, 0, caret.line, caret.line.text.Length);
2907 Invalidate(selection_prev.line, selection_prev.line.text.Length, caret.line, 0);
2910 if (caret.line.line_no <= selection_anchor.line.line_no) {
2911 selection_start.line = caret.line;
2912 selection_start.tag = caret.line.tags;
2913 selection_start.pos = 0;
2915 selection_end.line = selection_anchor.line;
2916 selection_end.tag = selection_anchor.tag;
2917 selection_end.pos = selection_anchor.pos;
2919 selection_end_anchor = true;
2921 selection_start.line = selection_anchor.line;
2922 selection_start.pos = selection_anchor.height;
2923 selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height);
2925 selection_end.line = caret.line;
2926 selection_end.tag = caret.line.tags;
2927 selection_end.pos = caret.line.text.Length;
2929 selection_end_anchor = false;
2931 selection_prev.line = caret.line;
2932 selection_prev.tag = caret.tag;
2933 selection_prev.pos = caret.pos;
2938 case CaretSelection.Word: {
2942 start_pos = FindWordSeparator(caret.line, caret.pos, false);
2943 end_pos = FindWordSeparator(caret.line, caret.pos, true);
2946 // Invalidate the selection delta
2947 if (caret > selection_prev) {
2948 Invalidate(selection_prev.line, selection_prev.pos, caret.line, end_pos);
2950 Invalidate(selection_prev.line, selection_prev.pos, caret.line, start_pos);
2952 if (caret < selection_anchor) {
2953 selection_start.line = caret.line;
2954 selection_start.tag = caret.line.FindTag(start_pos);
2955 selection_start.pos = start_pos;
2957 selection_end.line = selection_anchor.line;
2958 selection_end.tag = selection_anchor.tag;
2959 selection_end.pos = selection_anchor.pos;
2961 selection_prev.line = caret.line;
2962 selection_prev.tag = caret.tag;
2963 selection_prev.pos = start_pos;
2965 selection_end_anchor = true;
2967 selection_start.line = selection_anchor.line;
2968 selection_start.pos = selection_anchor.height;
2969 selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height);
2971 selection_end.line = caret.line;
2972 selection_end.tag = caret.line.FindTag(end_pos);
2973 selection_end.pos = end_pos;
2975 selection_prev.line = caret.line;
2976 selection_prev.tag = caret.tag;
2977 selection_prev.pos = end_pos;
2979 selection_end_anchor = false;
2984 case CaretSelection.Position: {
2985 SetSelectionToCaret(false);
2990 // We're setting the selection 'around' the caret position
2992 case CaretSelection.Line: {
2993 this.Invalidate(caret.line, 0, caret.line, caret.line.text.Length);
2995 selection_start.line = caret.line;
2996 selection_start.tag = caret.line.tags;
2997 selection_start.pos = 0;
2999 selection_end.line = caret.line;
3000 selection_end.pos = caret.line.text.Length;
3001 selection_end.tag = caret.line.FindTag(selection_end.pos);
3003 selection_anchor.line = selection_end.line;
3004 selection_anchor.tag = selection_end.tag;
3005 selection_anchor.pos = selection_end.pos;
3006 selection_anchor.height = 0;
3008 selection_prev.line = caret.line;
3009 selection_prev.tag = caret.tag;
3010 selection_prev.pos = caret.pos;
3012 this.selection_end_anchor = true;
3017 case CaretSelection.Word: {
3021 start_pos = FindWordSeparator(caret.line, caret.pos, false);
3022 end_pos = FindWordSeparator(caret.line, caret.pos, true);
3024 this.Invalidate(selection_start.line, start_pos, caret.line, end_pos);
3026 selection_start.line = caret.line;
3027 selection_start.tag = caret.line.FindTag(start_pos);
3028 selection_start.pos = start_pos;
3030 selection_end.line = caret.line;
3031 selection_end.tag = caret.line.FindTag(end_pos);
3032 selection_end.pos = end_pos;
3034 selection_anchor.line = selection_end.line;
3035 selection_anchor.tag = selection_end.tag;
3036 selection_anchor.pos = selection_end.pos;
3037 selection_anchor.height = start_pos;
3039 selection_prev.line = caret.line;
3040 selection_prev.tag = caret.tag;
3041 selection_prev.pos = caret.pos;
3043 this.selection_end_anchor = true;
3050 SetSelectionVisible (!(selection_start == selection_end));
3053 internal void SetSelectionToCaret(bool start) {
3055 // Invalidate old selection; selection is being reset to empty
3056 this.Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3058 selection_start.line = caret.line;
3059 selection_start.tag = caret.tag;
3060 selection_start.pos = caret.pos;
3062 // start always also selects end
3063 selection_end.line = caret.line;
3064 selection_end.tag = caret.tag;
3065 selection_end.pos = caret.pos;
3067 selection_anchor.line = caret.line;
3068 selection_anchor.tag = caret.tag;
3069 selection_anchor.pos = caret.pos;
3071 // Invalidate from previous end to caret (aka new end)
3072 if (selection_end_anchor) {
3073 if (selection_start != caret) {
3074 this.Invalidate(selection_start.line, selection_start.pos, caret.line, caret.pos);
3077 if (selection_end != caret) {
3078 this.Invalidate(selection_end.line, selection_end.pos, caret.line, caret.pos);
3082 if (caret < selection_anchor) {
3083 selection_start.line = caret.line;
3084 selection_start.tag = caret.tag;
3085 selection_start.pos = caret.pos;
3087 selection_end.line = selection_anchor.line;
3088 selection_end.tag = selection_anchor.tag;
3089 selection_end.pos = selection_anchor.pos;
3091 selection_end_anchor = true;
3093 selection_start.line = selection_anchor.line;
3094 selection_start.tag = selection_anchor.tag;
3095 selection_start.pos = selection_anchor.pos;
3097 selection_end.line = caret.line;
3098 selection_end.tag = caret.tag;
3099 selection_end.pos = caret.pos;
3101 selection_end_anchor = false;
3105 SetSelectionVisible (!(selection_start == selection_end));
3108 internal void SetSelection(Line start, int start_pos, Line end, int end_pos) {
3109 if (selection_visible) {
3110 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3113 if ((end.line_no < start.line_no) || ((end == start) && (end_pos <= start_pos))) {
3114 selection_start.line = end;
3115 selection_start.tag = LineTag.FindTag(end, end_pos);
3116 selection_start.pos = end_pos;
3118 selection_end.line = start;
3119 selection_end.tag = LineTag.FindTag(start, start_pos);
3120 selection_end.pos = start_pos;
3122 selection_end_anchor = true;
3124 selection_start.line = start;
3125 selection_start.tag = LineTag.FindTag(start, start_pos);
3126 selection_start.pos = start_pos;
3128 selection_end.line = end;
3129 selection_end.tag = LineTag.FindTag(end, end_pos);
3130 selection_end.pos = end_pos;
3132 selection_end_anchor = false;
3135 selection_anchor.line = start;
3136 selection_anchor.tag = selection_start.tag;
3137 selection_anchor.pos = start_pos;
3139 if (((start == end) && (start_pos == end_pos)) || start == null || end == null) {
3140 SetSelectionVisible (false);
3142 SetSelectionVisible (true);
3143 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3147 internal void SetSelectionStart(Line start, int start_pos) {
3148 // Invalidate from the previous to the new start pos
3149 Invalidate(selection_start.line, selection_start.pos, start, start_pos);
3151 selection_start.line = start;
3152 selection_start.pos = start_pos;
3153 selection_start.tag = LineTag.FindTag(start, start_pos);
3155 selection_anchor.line = start;
3156 selection_anchor.pos = start_pos;
3157 selection_anchor.tag = selection_start.tag;
3159 selection_end_anchor = false;
3162 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
3163 SetSelectionVisible (true);
3165 SetSelectionVisible (false);
3168 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3171 internal void SetSelectionStart(int character_index) {
3176 if (character_index < 0) {
3180 CharIndexToLineTag(character_index, out line, out tag, out pos);
3181 SetSelectionStart(line, pos);
3184 internal void SetSelectionEnd(Line end, int end_pos) {
3186 if (end == selection_end.line && end_pos == selection_start.pos) {
3187 selection_anchor.line = selection_start.line;
3188 selection_anchor.tag = selection_start.tag;
3189 selection_anchor.pos = selection_start.pos;
3191 selection_end.line = selection_start.line;
3192 selection_end.tag = selection_start.tag;
3193 selection_end.pos = selection_start.pos;
3195 selection_end_anchor = false;
3196 } else if ((end.line_no < selection_anchor.line.line_no) || ((end == selection_anchor.line) && (end_pos <= selection_anchor.pos))) {
3197 selection_start.line = end;
3198 selection_start.tag = LineTag.FindTag(end, end_pos);
3199 selection_start.pos = end_pos;
3201 selection_end.line = selection_anchor.line;
3202 selection_end.tag = selection_anchor.tag;
3203 selection_end.pos = selection_anchor.pos;
3205 selection_end_anchor = true;
3207 selection_start.line = selection_anchor.line;
3208 selection_start.tag = selection_anchor.tag;
3209 selection_start.pos = selection_anchor.pos;
3211 selection_end.line = end;
3212 selection_end.tag = LineTag.FindTag(end, end_pos);
3213 selection_end.pos = end_pos;
3215 selection_end_anchor = false;
3218 if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) {
3219 SetSelectionVisible (true);
3220 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3222 SetSelectionVisible (false);
3223 // ?? Do I need to invalidate here, tests seem to work without it, but I don't think they should :-s
3227 internal void SetSelectionEnd(int character_index) {
3232 if (character_index < 0) {
3236 CharIndexToLineTag(character_index, out line, out tag, out pos);
3237 SetSelectionEnd(line, pos);
3240 internal void SetSelection(Line start, int start_pos) {
3241 if (selection_visible) {
3242 Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3245 selection_start.line = start;
3246 selection_start.pos = start_pos;
3247 selection_start.tag = LineTag.FindTag(start, start_pos);
3249 selection_end.line = start;
3250 selection_end.tag = selection_start.tag;
3251 selection_end.pos = start_pos;
3253 selection_anchor.line = start;
3254 selection_anchor.tag = selection_start.tag;
3255 selection_anchor.pos = start_pos;
3257 selection_end_anchor = false;
3258 SetSelectionVisible (false);
3261 internal void InvalidateSelectionArea() {
3262 Invalidate (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos);
3265 // Return the current selection, as string
3266 internal string GetSelection() {
3267 // We return String.Empty if there is no selection
3268 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
3269 return string.Empty;
3272 if (!multiline || (selection_start.line == selection_end.line)) {
3273 return selection_start.line.text.ToString(selection_start.pos, selection_end.pos - selection_start.pos);
3280 sb = new StringBuilder();
3281 start = selection_start.line.line_no;
3282 end = selection_end.line.line_no;
3284 sb.Append(selection_start.line.text.ToString(selection_start.pos, selection_start.line.text.Length - selection_start.pos) + Environment.NewLine);
3286 if ((start + 1) < end) {
3287 for (i = start + 1; i < end; i++) {
3288 sb.Append(GetLine(i).text.ToString() + Environment.NewLine);
3292 sb.Append(selection_end.line.text.ToString(0, selection_end.pos));
3294 return sb.ToString();
3298 internal void ReplaceSelection(string s, bool select_new) {
3301 int selection_start_pos = LineTagToCharIndex (selection_start.line, selection_start.pos);
3302 // First, delete any selected text
3303 if ((selection_start.pos != selection_end.pos) || (selection_start.line != selection_end.line)) {
3304 if (!multiline || (selection_start.line == selection_end.line)) {
3305 undo.RecordDeleteChars(selection_start.line, selection_start.pos + 1, selection_end.pos - selection_start.pos);
3307 DeleteChars(selection_start.tag, selection_start.pos, selection_end.pos - selection_start.pos);
3309 // The tag might have been removed, we need to recalc it
3310 selection_start.tag = selection_start.line.FindTag(selection_start.pos);
3315 start = selection_start.line.line_no;
3316 end = selection_end.line.line_no;
3318 undo.RecordDelete(selection_start.line, selection_start.pos + 1, selection_end.line, selection_end.pos);
3320 // Delete first line
3321 DeleteChars(selection_start.tag, selection_start.pos, selection_start.line.text.Length - selection_start.pos);
3324 DeleteChars(selection_end.line.tags, 0, selection_end.pos);
3328 for (i = end - 1; i >= start; i--) {
3333 // BIG FAT WARNING - selection_end.line might be stale due
3334 // to the above Delete() call. DONT USE IT before hitting the end of this method!
3336 // Join start and end
3337 Combine(selection_start.line.line_no, start);
3341 Insert(selection_start.line, null, selection_start.pos, true, s);
3344 CharIndexToLineTag(selection_start_pos + s.Length, out selection_start.line,
3345 out selection_start.tag, out selection_start.pos);
3347 selection_end.line = selection_start.line;
3348 selection_end.pos = selection_start.pos;
3349 selection_end.tag = selection_start.tag;
3350 selection_anchor.line = selection_start.line;
3351 selection_anchor.pos = selection_start.pos;
3352 selection_anchor.tag = selection_start.tag;
3354 SetSelectionVisible (false);
3356 CharIndexToLineTag(selection_start_pos, out selection_start.line,
3357 out selection_start.tag, out selection_start.pos);
3359 CharIndexToLineTag(selection_start_pos + s.Length, out selection_end.line,
3360 out selection_end.tag, out selection_end.pos);
3362 selection_anchor.line = selection_start.line;
3363 selection_anchor.pos = selection_start.pos;
3364 selection_anchor.tag = selection_start.tag;
3366 SetSelectionVisible (true);
3370 internal void CharIndexToLineTag(int index, out Line line_out, out LineTag tag_out, out int pos) {
3379 for (i = 1; i <= lines; i++) {
3383 chars += line.text.Length + crlf_size;
3385 if (index <= chars) {
3386 // we found the line
3389 while (tag != null) {
3390 if (index < (start + tag.start + tag.length)) {
3392 tag_out = LineTag.GetFinalTag (tag);
3393 pos = index - start;
3396 if (tag.next == null) {
3399 next_line = GetLine(line.line_no + 1);
3401 if (next_line != null) {
3402 line_out = next_line;
3403 tag_out = LineTag.GetFinalTag (next_line.tags);
3408 tag_out = LineTag.GetFinalTag (tag);
3409 pos = line_out.text.Length;
3418 line_out = GetLine(lines);
3419 tag = line_out.tags;
3420 while (tag.next != null) {
3424 pos = line_out.text.Length;
3427 internal int LineTagToCharIndex(Line line, int pos) {
3431 // Count first and last line
3434 // Count the lines in the middle
3436 for (i = 1; i < line.line_no; i++) {
3437 length += GetLine(i).text.Length + crlf_size;
3445 internal int SelectionLength() {
3446 if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) {
3450 if (!multiline || (selection_start.line == selection_end.line)) {
3451 return selection_end.pos - selection_start.pos;
3458 // Count first and last line
3459 length = selection_start.line.text.Length - selection_start.pos + selection_end.pos + crlf_size;
3461 // Count the lines in the middle
3462 start = selection_start.line.line_no + 1;
3463 end = selection_end.line.line_no;
3466 for (i = start; i < end; i++) {
3467 length += GetLine(i).text.Length + crlf_size;
3478 /// <summary>Give it a Line number and it returns the Line object at with that line number</summary>
3479 internal Line GetLine(int LineNo) {
3480 Line line = document;
3482 while (line != sentinel) {
3483 if (LineNo == line.line_no) {
3485 } else if (LineNo < line.line_no) {
3495 /// <summary>Retrieve the previous tag; walks line boundaries</summary>
3496 internal LineTag PreviousTag(LineTag tag) {
3499 if (tag.previous != null) {
3500 return tag.previous;
3504 if (tag.line.line_no == 1) {
3508 l = GetLine(tag.line.line_no - 1);
3513 while (t.next != null) {
3522 /// <summary>Retrieve the next tag; walks line boundaries</summary>
3523 internal LineTag NextTag(LineTag tag) {
3526 if (tag.next != null) {
3531 l = GetLine(tag.line.line_no + 1);
3539 internal Line ParagraphStart(Line line) {
3540 while (line.soft_break) {
3541 line = GetLine(line.line_no - 1);
3546 internal Line ParagraphEnd(Line line) {
3549 while (line.soft_break) {
3550 l = GetLine(line.line_no + 1);
3551 if ((l == null) || (!l.soft_break)) {
3559 /// <summary>Give it a Y pixel coordinate and it returns the Line covering that Y coordinate</summary>
3560 internal Line GetLineByPixel(int y, bool exact) {
3561 Line line = document;
3564 while (line != sentinel) {
3566 if ((y >= line.Y) && (y < (line.Y+line.height))) {
3568 } else if (y < line.Y) {
3581 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
3582 internal LineTag FindTag(int x, int y, out int index, bool exact) {
3586 line = GetLineByPixel(y, exact);
3593 // Alignment adjustment
3594 x += line.align_shift;
3597 if (x >= tag.X && x < (tag.X+tag.width)) {
3600 end = tag.start + tag.length - 1;
3602 for (int pos = tag.start; pos < end; pos++) {
3603 if (x < line.widths[pos]) {
3605 return LineTag.GetFinalTag (tag);
3609 return LineTag.GetFinalTag (tag);
3611 if (tag.next != null) {
3619 index = line.text.Length;
3620 return LineTag.GetFinalTag (tag);
3625 // Give it x/y pixel coordinates and it returns the Tag at that position; optionally the char position is returned in index
3626 internal LineTag FindCursor(int x, int y, out int index) {
3630 line = GetLineByPixel(y, false);
3633 // Adjust for alignment
3634 x -= line.align_shift;
3637 if (x >= tag.X && x < (tag.X+tag.width)) {
3640 end = tag.start + tag.length - 1;
3642 for (int pos = tag.start-1; pos < end; pos++) {
3643 // When clicking on a character, we position the cursor to whatever edge
3644 // of the character the click was closer
3645 if (x < (line.widths[pos] + ((line.widths[pos+1]-line.widths[pos])/2))) {
3653 if (tag.next != null) {
3656 index = line.text.Length;
3662 /// <summary>Format area of document in specified font and color</summary>
3663 /// <param name="start_pos">1-based start position on start_line</param>
3664 /// <param name="end_pos">1-based end position on end_line </param>
3665 internal void FormatText(Line start_line, int start_pos, Line end_line, int end_pos, Font font, Brush color) {
3668 // First, format the first line
3669 if (start_line != end_line) {
3671 LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, font, color);
3674 LineTag.FormatText(end_line, 1, end_pos, font, color);
3676 // Now all the lines inbetween
3677 for (int i = start_line.line_no + 1; i < end_line.line_no; i++) {
3679 LineTag.FormatText(l, 1, l.text.Length, font, color);
3682 // Special case, single line
3683 LineTag.FormatText(start_line, start_pos, end_pos - start_pos, font, color);
3687 /// <summary>Re-format areas of the document in specified font and color</summary>
3688 /// <param name="start_pos">1-based start position on start_line</param>
3689 /// <param name="end_pos">1-based end position on end_line </param>
3690 /// <param name="font">Font specifying attributes</param>
3691 /// <param name="color">Color (or NULL) to apply</param>
3692 /// <param name="apply">Attributes from font and color to apply</param>
3693 internal void FormatText(Line start_line, int start_pos, Line end_line, int end_pos, FontDefinition attributes) {
3696 // First, format the first line
3697 if (start_line != end_line) {
3699 LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, attributes);
3702 LineTag.FormatText(end_line, 1, end_pos - 1, attributes);
3704 // Now all the lines inbetween
3705 for (int i = start_line.line_no + 1; i < end_line.line_no; i++) {
3707 LineTag.FormatText(l, 1, l.text.Length, attributes);
3710 // Special case, single line
3711 LineTag.FormatText(start_line, start_pos, end_pos - start_pos, attributes);
3715 internal void RecalculateAlignments() {
3721 while (line_no <= lines) {
3722 line = GetLine(line_no);
3725 switch (line.alignment) {
3726 case HorizontalAlignment.Left:
3727 line.align_shift = 0;
3729 case HorizontalAlignment.Center:
3730 line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2;
3732 case HorizontalAlignment.Right:
3733 line.align_shift = viewport_width - (int)line.widths[line.text.Length];
3743 /// <summary>Calculate formatting for the whole document</summary>
3744 internal bool RecalculateDocument(Graphics g) {
3745 return RecalculateDocument(g, 1, this.lines, false);
3748 /// <summary>Calculate formatting starting at a certain line</summary>
3749 internal bool RecalculateDocument(Graphics g, int start) {
3750 return RecalculateDocument(g, start, this.lines, false);
3753 /// <summary>Calculate formatting within two given line numbers</summary>
3754 internal bool RecalculateDocument(Graphics g, int start, int end) {
3755 return RecalculateDocument(g, start, end, false);
3758 /// <summary>With optimize on, returns true if line heights changed</summary>
3759 internal bool RecalculateDocument(Graphics g, int start, int end, bool optimize) {
3768 recalc_pending = true;
3769 recalc_start = start;
3771 recalc_optimize = optimize;
3775 Y = GetLine(start).Y;
3780 changed = true; // We always return true if we run non-optimized
3785 while (line_no <= (end + this.lines - shift)) {
3786 line = GetLine(line_no++);
3791 line.RecalculateLine(g, this);
3793 if (line.recalc && line.RecalculateLine(g, this)) {
3795 // If the height changed, all subsequent lines change
3802 line.RecalculatePasswordLine(g, this);
3804 if (line.recalc && line.RecalculatePasswordLine(g, this)) {
3806 // If the height changed, all subsequent lines change
3813 if (line.widths[line.text.Length] > new_width) {
3814 new_width = (int)line.widths[line.text.Length];
3817 // Calculate alignment
3818 if (line.alignment != HorizontalAlignment.Left) {
3819 if (line.alignment == HorizontalAlignment.Center) {
3820 line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2;
3822 line.align_shift = viewport_width - (int)line.widths[line.text.Length] - 1;
3828 if (line_no > lines) {
3833 if (document_x != new_width) {
3834 document_x = new_width;
3835 if (WidthChanged != null) {
3836 WidthChanged(this, null);
3840 RecalculateAlignments();
3842 line = GetLine(lines);
3844 if (document_y != line.Y + line.height) {
3845 document_y = line.Y + line.height;
3846 if (HeightChanged != null) {
3847 HeightChanged(this, null);
3854 internal int Size() {
3858 private void owner_HandleCreated(object sender, EventArgs e) {
3859 RecalculateDocument(owner.CreateGraphicsInternal());
3863 private void owner_VisibleChanged(object sender, EventArgs e) {
3864 if (owner.Visible) {
3865 RecalculateDocument(owner.CreateGraphicsInternal());
3869 internal static bool IsWordSeparator(char ch) {
3883 internal int FindWordSeparator(Line line, int pos, bool forward) {
3886 len = line.text.Length;
3889 for (int i = pos + 1; i < len; i++) {
3890 if (IsWordSeparator(line.Text[i])) {
3896 for (int i = pos - 1; i > 0; i--) {
3897 if (IsWordSeparator(line.Text[i - 1])) {
3905 /* Search document for text */
3906 internal bool FindChars(char[] chars, Marker start, Marker end, out Marker result) {
3912 // Search for occurence of any char in the chars array
3913 result = new Marker();
3916 line_no = start.line.line_no;
3918 while (line_no <= end.line.line_no) {
3919 line_len = line.text.Length;
3920 while (pos < line_len) {
3921 for (int i = 0; i < chars.Length; i++) {
3922 if (line.text[pos] == chars[i]) {
3924 if ((line.line_no == end.line.line_no) && (pos >= end.pos)) {
3938 line = GetLine(line_no);
3944 // This version does not build one big string for searching, instead it handles
3945 // line-boundaries, which is faster and less memory intensive
3946 // FIXME - Depending on culture stuff we might have to create a big string and use culturespecific
3947 // search stuff and change it to accept and return positions instead of Markers (which would match
3948 // RichTextBox behaviour better but would be inconsistent with the rest of TextControl)
3949 internal bool Find(string search, Marker start, Marker end, out Marker result, RichTextBoxFinds options) {
3951 string search_string;
3963 result = new Marker();
3964 word_option = ((options & RichTextBoxFinds.WholeWord) != 0);
3965 ignore_case = ((options & RichTextBoxFinds.MatchCase) == 0);
3966 reverse = ((options & RichTextBoxFinds.Reverse) != 0);
3969 line_no = start.line.line_no;
3973 // Prep our search string, lowercasing it if we do case-independent matching
3976 sb = new StringBuilder(search);
3977 for (int i = 0; i < sb.Length; i++) {
3978 sb[i] = Char.ToLower(sb[i]);
3980 search_string = sb.ToString();
3982 search_string = search;
3985 // We need to check if the character before our start position is a wordbreak
3988 if ((pos == 0) || (IsWordSeparator(line.text[pos - 1]))) {
3995 if (IsWordSeparator(line.text[pos - 1])) {
4001 // Need to check the end of the previous line
4004 prev_line = GetLine(line_no - 1);
4005 if (prev_line.soft_break) {
4006 if (IsWordSeparator(prev_line.text[prev_line.text.Length - 1])) {
4020 // To avoid duplication of this loop with reverse logic, we search
4021 // through the document, remembering the last match and when returning
4022 // report that last remembered match
4024 last = new Marker();
4025 last.height = -1; // Abused - we use it to track change
4027 while (line_no <= end.line.line_no) {
4028 if (line_no != end.line.line_no) {
4029 line_len = line.text.Length;
4034 while (pos < line_len) {
4035 if (word_option && (current == search_string.Length)) {
4036 if (IsWordSeparator(line.text[pos])) {
4049 c = Char.ToLower(line.text[pos]);
4054 if (c == search_string[current]) {
4059 if (!word_option || (word_option && (word || (current > 0)))) {
4063 if (!word_option && (current == search_string.Length)) {
4080 if (IsWordSeparator(c)) {
4088 // Mark that we just saw a word boundary
4089 if (!line.soft_break) {
4093 if (current == search_string.Length) {
4109 line = GetLine(line_no);
4113 if (last.height != -1) {
4123 // if ((line.line_no == end.line.line_no) && (pos >= end.pos)) {
4135 internal void GetMarker(out Marker mark, bool start) {
4136 mark = new Marker();
4139 mark.line = GetLine(1);
4140 mark.tag = mark.line.tags;
4143 mark.line = GetLine(lines);
4144 mark.tag = mark.line.tags;
4145 while (mark.tag.next != null) {
4146 mark.tag = mark.tag.next;
4148 mark.pos = mark.line.text.Length;
4151 #endregion // Internal Methods
4154 internal event EventHandler CaretMoved;
4155 internal event EventHandler WidthChanged;
4156 internal event EventHandler HeightChanged;
4157 internal event EventHandler LengthChanged;
4158 #endregion // Events
4160 #region Administrative
4161 public IEnumerator GetEnumerator() {
4166 public override bool Equals(object obj) {
4171 if (!(obj is Document)) {
4179 if (ToString().Equals(((Document)obj).ToString())) {
4186 public override int GetHashCode() {
4190 public override string ToString() {
4191 return "document " + this.document_id;
4193 #endregion // Administrative
4196 internal class LineTag {
4197 #region Local Variables;
4198 // Payload; formatting
4199 internal Font font; // System.Drawing.Font object for this tag
4200 internal Brush color; // System.Drawing.Brush object
4203 internal int start; // start, in chars; index into Line.text
4204 internal int length; // length, in chars
4205 internal bool r_to_l; // Which way is the font
4208 internal int height; // Height in pixels of the text this tag describes
4209 internal int X; // X location of the text this tag describes
4210 internal float width; // Width in pixels of the text this tag describes
4211 internal int ascent; // Ascent of the font for this tag
4212 internal int shift; // Shift down for this tag, to stay on baseline
4215 internal Line line; // The line we're on
4216 internal LineTag next; // Next tag on the same line
4217 internal LineTag previous; // Previous tag on the same line
4220 #region Constructors
4221 internal LineTag(Line line, int start, int length) {
4224 this.length = length;
4228 #endregion // Constructors
4230 #region Internal Methods
4231 ///<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>
4232 internal LineTag Break(int pos) {
4236 if (pos == this.start) {
4238 } else if (pos >= (start + length)) {
4242 new_tag = new LineTag(line, pos, start + length - pos);
4243 new_tag.color = color;
4244 new_tag.font = font;
4245 this.length -= new_tag.length;
4246 new_tag.next = this.next;
4247 this.next = new_tag;
4248 new_tag.previous = this;
4249 if (new_tag.next != null) {
4250 new_tag.next.previous = new_tag;
4256 ///<summary>Create new font and brush from existing font and given new attributes. Returns true if fontheight changes</summary>
4257 internal static bool GenerateTextFormat(Font font_from, Brush color_from, FontDefinition attributes, out Font new_font, out Brush new_color) {
4263 if (attributes.font_obj == null) {
4264 size = font_from.SizeInPoints;
4265 unit = font_from.Unit;
4266 face = font_from.Name;
4267 style = font_from.Style;
4269 if (attributes.face != null) {
4270 face = attributes.face;
4273 if (attributes.size != 0) {
4274 size = attributes.size;
4277 style |= attributes.add_style;
4278 style &= ~attributes.remove_style;
4281 new_font = new Font(face, size, style, unit);
4283 new_font = attributes.font_obj;
4286 // Create 'new' color brush
4287 if (attributes.color != Color.Empty) {
4288 new_color = new SolidBrush(attributes.color);
4290 new_color = color_from;
4293 if (new_font.Height == font_from.Height) {
4299 /// <summary>Applies 'font' and 'brush' to characters starting at 'start' for 'length' chars;
4300 /// Removes any previous tags overlapping the same area;
4301 /// returns true if lineheight has changed</summary>
4302 /// <param name="start">1-based character position on line</param>
4303 internal static bool FormatText(Line line, int start, int length, Font font, Brush color) {
4308 bool retval = false; // Assume line-height doesn't change
4311 if (font.Height != line.height) {
4314 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
4316 // A little sanity, not sure if it's needed, might be able to remove for speed
4317 if (length > line.text.Length) {
4318 length = line.text.Length;
4322 end = start + length;
4324 // Common special case
4325 if ((start == 1) && (length == tag.length)) {
4332 //Console.WriteLine("Finding tag for {0} {1}", line, start);
4333 start_tag = FindTag(line, start);
4334 end_tag = FindTag (line, end);
4336 if (start_tag == null) { // FIXME - is there a better way to handle this, or do we even need it?
4337 throw new Exception(String.Format("Could not find start_tag in document at line {0} position {1}", line.line_no, start));
4340 tag = new LineTag(line, start, length);
4347 //Console.WriteLine("Start tag: '{0}'", start_tag!=null ? start_tag.ToString() : "NULL");
4348 if (start_tag.start == start) {
4349 tag.next = start_tag;
4350 tag.previous = start_tag.previous;
4351 if (start_tag.previous != null) {
4352 start_tag.previous.next = tag;
4354 start_tag.previous = tag;
4358 if (end_tag != null) {
4359 // Shorten up the end tag
4360 end_tag.previous = tag;
4361 end_tag.length = end - start_tag.start + start_tag.length;
4362 end_tag.start = end;
4368 while (tag != end_tag) {
4369 if ((tag.start + tag.length) <= end) {
4371 tag.previous.next = tag.next;
4372 if (tag.next != null) {
4373 tag.next.previous = tag.previous;
4383 /// <summary>Applies font attributes specified to characters starting at 'start' for 'length' chars;
4384 /// Breaks tags at start and end point, keeping middle tags with altered attributes.
4385 /// Returns true if lineheight has changed</summary>
4386 /// <param name="start">1-based character position on line</param>
4387 internal static bool FormatText(Line line, int start, int length, FontDefinition attributes) {
4391 bool retval = false; // Assume line-height doesn't change
4393 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
4395 // A little sanity, not sure if it's needed, might be able to remove for speed
4396 if (length > line.text.Length) {
4397 length = line.text.Length;
4402 // Common special case
4403 if ((start == 1) && (length == tag.length)) {
4405 GenerateTextFormat(tag.font, tag.color, attributes, out tag.font, out tag.color);
4409 start_tag = FindTag(line, start);
4411 if (start_tag == null) {
4413 // We are 'starting' after all valid tags; create a new tag with the right attributes
4414 start_tag = FindTag(line, line.text.Length - 1);
4415 start_tag.next = new LineTag(line, line.text.Length + 1, 0);
4416 start_tag.next.font = start_tag.font;
4417 start_tag.next.color = start_tag.color;
4418 start_tag.next.previous = start_tag;
4419 start_tag = start_tag.next;
4421 throw new Exception(String.Format("Could not find start_tag in document at line {0} position {1}", line.line_no, start));
4424 start_tag = start_tag.Break(start);
4427 end_tag = FindTag(line, start + length);
4428 if (end_tag != null) {
4429 end_tag = end_tag.Break(start + length);
4432 // start_tag or end_tag might be null; we're cool with that
4433 // we now walk from start_tag to end_tag, applying new attributes
4435 while ((tag != null) && tag != end_tag) {
4436 if (LineTag.GenerateTextFormat(tag.font, tag.color, attributes, out tag.font, out tag.color)) {
4445 /// <summary>Finds the tag that describes the character at position 'pos' on 'line'</summary>
4446 internal static LineTag FindTag(Line line, int pos) {
4447 LineTag tag = line.tags;
4449 // Beginning of line is a bit special
4451 // Not sure if we should get the final tag here
4455 while (tag != null) {
4456 if ((tag.start <= pos) && (pos < (tag.start+tag.length))) {
4457 return GetFinalTag (tag);
4466 // There can be multiple tags at the same position, we want to make
4467 // sure we are using the very last tag at the given position
4468 internal static LineTag GetFinalTag (LineTag tag)
4472 while (res.next != null && res.next.length == 0)
4477 /// <summary>Combines 'this' tag with 'other' tag</summary>
4478 internal bool Combine(LineTag other) {
4479 if (!this.Equals(other)) {
4483 this.width += other.width;
4484 this.length += other.length;
4485 this.next = other.next;
4486 if (this.next != null) {
4487 this.next.previous = this;
4494 /// <summary>Remove 'this' tag ; to be called when formatting is to be removed</summary>
4495 internal bool Remove() {
4496 if ((this.start == 1) && (this.next == null)) {
4497 // We cannot remove the only tag
4500 if (this.start != 1) {
4501 this.previous.length += this.length;
4502 this.previous.width = -1;
4503 this.previous.next = this.next;
4504 this.next.previous = this.previous;
4506 this.next.start = 1;
4507 this.next.length += this.length;
4508 this.next.width = -1;
4509 this.line.tags = this.next;
4510 this.next.previous = null;
4516 /// <summary>Checks if 'this' tag describes the same formatting options as 'obj'</summary>
4517 public override bool Equals(object obj) {
4524 if (!(obj is LineTag)) {
4532 other = (LineTag)obj;
4534 if (this.font.Equals(other.font) && this.color.Equals(other.color)) { // FIXME add checking for things like link or type later
4541 public override int GetHashCode() {
4542 return base.GetHashCode ();
4545 public override string ToString() {
4547 return "Tag starts at index " + this.start + "length " + this.length + " text: " + this.line.Text.Substring(this.start-1, this.length) + "Font " + this.font.ToString();
4548 return "Zero Lengthed tag at index " + this.start;
4551 #endregion // Internal Methods
4554 internal class UndoClass {
4555 internal enum ActionType {
4564 internal class Action {
4565 internal ActionType type;
4566 internal int line_no;
4568 internal object data;
4571 #region Local Variables
4572 private Document document;
4573 private Stack undo_actions;
4574 private Stack redo_actions;
4575 private int caret_line;
4576 private int caret_pos;
4577 #endregion // Local Variables
4579 #region Constructors
4580 internal UndoClass(Document doc) {
4582 undo_actions = new Stack(50);
4583 redo_actions = new Stack(50);
4585 #endregion // Constructors
4588 [MonoTODO("Change this to be configurable")]
4589 internal int UndoLevels {
4591 return undo_actions.Count;
4595 [MonoTODO("Change this to be configurable")]
4596 internal int RedoLevels {
4598 return redo_actions.Count;
4602 [MonoTODO("Come up with good naming and localization")]
4603 internal string UndoName {
4607 action = (Action)undo_actions.Peek();
4608 switch(action.type) {
4609 case ActionType.InsertChar: {
4610 Locale.GetText("Insert character");
4614 case ActionType.DeleteChar: {
4615 Locale.GetText("Delete character");
4619 case ActionType.InsertString: {
4620 Locale.GetText("Insert string");
4624 case ActionType.DeleteChars: {
4625 Locale.GetText("Delete string");
4629 case ActionType.CursorMove: {
4630 Locale.GetText("Cursor move");
4638 internal string RedoName() {
4641 #endregion // Properties
4643 #region Internal Methods
4644 internal void Clear() {
4645 undo_actions.Clear();
4646 redo_actions.Clear();
4649 internal void Undo() {
4652 if (undo_actions.Count == 0) {
4656 action = (Action)undo_actions.Pop();
4658 // Put onto redo stack
4659 redo_actions.Push(action);
4662 switch(action.type) {
4663 case ActionType.InsertChar: {
4664 // FIXME - implement me
4668 case ActionType.DeleteChars: {
4669 this.Insert(document.GetLine(action.line_no), action.pos, (Line)action.data);
4670 Undo(); // Grab the cursor location
4674 case ActionType.CursorMove: {
4675 document.caret.line = document.GetLine(action.line_no);
4676 if (document.caret.line == null) {
4681 document.caret.tag = document.caret.line.FindTag(action.pos);
4682 document.caret.pos = action.pos;
4683 document.caret.height = document.caret.tag.height;
4685 if (document.owner.IsHandleCreated) {
4686 XplatUI.DestroyCaret(document.owner.Handle);
4687 XplatUI.CreateCaret(document.owner.Handle, 2, document.caret.height);
4688 XplatUI.SetCaretPos(document.owner.Handle, (int)document.caret.tag.line.widths[document.caret.pos] + document.caret.line.align_shift - document.viewport_x, document.caret.line.Y + document.caret.tag.shift - document.viewport_y + Document.caret_shift);
4690 document.DisplayCaret ();
4693 // FIXME - enable call
4694 //if (document.CaretMoved != null) document.CaretMoved(this, EventArgs.Empty);
4700 internal void Redo() {
4701 if (redo_actions.Count == 0) {
4705 #endregion // Internal Methods
4707 #region Private Methods
4709 public void RecordDeleteChars(Line line, int pos, int length) {
4710 RecordDelete(line, pos, line, pos + length - 1);
4713 // start_pos, end_pos = 1 based
4714 public void RecordDelete(Line start_line, int start_pos, Line end_line, int end_pos) {
4718 l = Duplicate(start_line, start_pos, end_line, end_pos);
4721 a.type = ActionType.DeleteChars;
4723 a.line_no = start_line.line_no;
4724 a.pos = start_pos - 1;
4726 // Record the cursor position before, since the actions will occur in reverse order
4728 undo_actions.Push(a);
4731 public void RecordCursor() {
4732 if (document.caret.line == null) {
4736 RecordCursor(document.caret.line, document.caret.pos);
4739 public void RecordCursor(Line line, int pos) {
4742 if ((line.line_no == caret_line) && (pos == caret_pos)) {
4746 caret_line = line.line_no;
4750 a.type = ActionType.CursorMove;
4751 a.line_no = line.line_no;
4754 undo_actions.Push(a);
4757 // start_pos = 1-based
4758 // end_pos = 1-based
4759 public Line Duplicate(Line start_line, int start_pos, Line end_line, int end_pos) {
4764 LineTag current_tag;
4773 for (int i = start_line.line_no; i <= end_line.line_no; i++) {
4774 current = document.GetLine(i);
4776 if (start_line.line_no == i) {
4782 if (end_line.line_no == i) {
4785 end = current.text.Length;
4789 line.text = new StringBuilder(current.text.ToString(start - 1, end - start + 1));
4791 // Copy tags from start to start+length onto new line
4792 current_tag = current.FindTag(start - 1);
4793 while ((current_tag != null) && (current_tag.start < end)) {
4794 if ((current_tag.start <= start) && (start < (current_tag.start + current_tag.length))) {
4795 // start tag is within this tag
4798 tag_start = current_tag.start;
4801 if (end < (current_tag.start + current_tag.length)) {
4802 tag_length = end - tag_start + 1;
4804 tag_length = current_tag.start + current_tag.length - tag_start;
4806 tag = new LineTag(line, tag_start - start + 1, tag_length);
4807 tag.color = current_tag.color;
4808 tag.font = current_tag.font;
4810 current_tag = current_tag.next;
4812 // Add the new tag to the line
4813 if (line.tags == null) {
4819 while (tail.next != null) {
4823 tag.previous = tail;
4827 if ((i + 1) <= end_line.line_no) {
4828 line.soft_break = current.soft_break;
4830 // Chain them (we use right/left as next/previous)
4831 line.right = new Line();
4832 line.right.left = line;
4840 // Insert multi-line text at the given position; use formatting at insertion point for inserted text
4841 internal void Insert(Line line, int pos, Line insert) {
4848 // Handle special case first
4849 if (insert.right == null) {
4851 // Single line insert
4852 document.Split(line, pos);
4854 if (insert.tags == null) {
4855 return; // Blank line
4858 //Insert our tags at the end
4861 while (tag.next != null) {
4865 offset = tag.start + tag.length - 1;
4867 tag.next = insert.tags;
4868 line.text.Insert(offset, insert.text.ToString());
4870 // Adjust start locations
4872 while (tag != null) {
4873 tag.start += offset;
4877 // Put it back together
4878 document.Combine(line.line_no, line.line_no + 1);
4879 document.UpdateView(line, pos);
4886 while (current != null) {
4887 if (current == insert) {
4888 // Inserting the first line we split the line (and make space)
4889 document.Split(line, pos);
4890 //Insert our tags at the end of the line
4894 while (tag.next != null) {
4897 offset = tag.start + tag.length - 1;
4898 tag.next = current.tags;
4899 tag.next.previous = tag;
4905 line.tags = current.tags;
4906 line.tags.previous = null;
4910 document.Split(line.line_no, 0);
4912 line.tags = current.tags;
4913 line.tags.previous = null;
4916 // Adjust start locations and line pointers
4917 while (tag != null) {
4918 tag.start += offset;
4923 line.text.Insert(offset, current.text.ToString());
4924 line.Grow(line.text.Length);
4927 line = document.GetLine(line.line_no + 1);
4929 // FIXME? Test undo of line-boundaries
4930 if ((current.right == null) && (current.tags.length != 0)) {
4931 document.Combine(line.line_no - 1, line.line_no);
4933 current = current.right;
4938 // Recalculate our document
4939 document.UpdateView(first, lines, pos);
4942 #endregion // Private Methods