diff options
| author | Michael VanOverbeek <[email protected]> | 2016-07-25 12:57:52 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2016-07-25 12:57:52 -0400 |
| commit | 46c1c31302f111a1f3ec23a70e6f3986a9aa2a27 (patch) | |
| tree | f00af7ea3f6ad2641fb26fa1d310fd8b7179b39c /source/ShiftUI/Internal/TextControl.cs | |
| parent | af48e774189596b8d7a058c564a7d6d75205ca03 (diff) | |
| parent | 6fa16209519896de09949a27425dff00ebf2970a (diff) | |
| download | shiftos-c--46c1c31302f111a1f3ec23a70e6f3986a9aa2a27.tar.gz shiftos-c--46c1c31302f111a1f3ec23a70e6f3986a9aa2a27.tar.bz2 shiftos-c--46c1c31302f111a1f3ec23a70e6f3986a9aa2a27.zip | |
Merge pull request #17 from MichaelTheShifter/shiftui_integration
Shiftui integration
Diffstat (limited to 'source/ShiftUI/Internal/TextControl.cs')
| -rw-r--r-- | source/ShiftUI/Internal/TextControl.cs | 4613 |
1 files changed, 4613 insertions, 0 deletions
diff --git a/source/ShiftUI/Internal/TextControl.cs b/source/ShiftUI/Internal/TextControl.cs new file mode 100644 index 0000000..65dd5c6 --- /dev/null +++ b/source/ShiftUI/Internal/TextControl.cs @@ -0,0 +1,4613 @@ +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Copyright (c) 2004-2006 Novell, Inc. (http://www.novell.com) +// +// Authors: +// Peter Bartok [email protected] +// +// + +// NOT COMPLETE + +// There's still plenty of things missing, I've got most of it planned, just hadn't had +// the time to write it all yet. +// Stuff missing (in no particular order): +// - Align text after RecalculateLine +// - Implement tag types for hotlinks, etc. +// - Implement CaretPgUp/PgDown + +// NOTE: +// selection_start.pos and selection_end.pos are 0-based +// selection_start.pos = first selected char +// selection_end.pos = first NOT-selected char +// +// FormatText methods are 1-based (as are all tags, LineTag.Start is 1 for +// the first character on a line; the reason is that 0 is the position +// *before* the first character on a line + + +#undef Debug + +using System; +using System.Collections; +using System.Drawing; +using System.Drawing.Text; +using System.Text; +using RTF=ShiftUI.RTF; + +namespace ShiftUI { + internal enum LineColor { + Red = 0, + Black = 1 + } + + internal enum CaretSelection { + Position, // Selection=Caret + Word, // Selection=Word under caret + Line // Selection=Line under caret + } + + [Flags] + internal enum FormatSpecified { + None, + + BackColor = 2, + Font = 4, + Color = 8, + } + + internal enum CaretDirection { + CharForward, // Move a char to the right + CharBack, // Move a char to the left + LineUp, // Move a line up + LineDown, // Move a line down + Home, // Move to the beginning of the line + End, // Move to the end of the line + PgUp, // Move one page up + PgDn, // Move one page down + CtrlPgUp, // Move caret to the first visible char in the viewport + CtrlPgDn, // Move caret to the last visible char in the viewport + CtrlHome, // Move to the beginning of the document + CtrlEnd, // Move to the end of the document + WordBack, // Move to the beginning of the previous word (or beginning of line) + WordForward, // Move to the beginning of the next word (or end of line) + SelectionStart, // Move to the beginning of the current selection + SelectionEnd, // Move to the end of the current selection + CharForwardNoWrap, // Move a char forward, but don't wrap onto the next line + CharBackNoWrap // Move a char backward, but don't wrap onto the previous line + } + + internal enum LineEnding { + Wrap = 1, // line wraps to the next line + Limp = 2, // \r + Hard = 4, // \r\n + Soft = 8, // \r\r\n + Rich = 16, // \n + + None = 0 + } + + internal class Document : ICloneable, IEnumerable { + #region Structures + // FIXME - go through code and check for places where + // we do explicit comparisons instead of using the compare overloads + internal struct Marker { + internal Line line; + internal LineTag tag; + internal int pos; + internal int height; + + public static bool operator<(Marker lhs, Marker rhs) { + if (lhs.line.line_no < rhs.line.line_no) { + return true; + } + + if (lhs.line.line_no == rhs.line.line_no) { + if (lhs.pos < rhs.pos) { + return true; + } + } + return false; + } + + public static bool operator>(Marker lhs, Marker rhs) { + if (lhs.line.line_no > rhs.line.line_no) { + return true; + } + + if (lhs.line.line_no == rhs.line.line_no) { + if (lhs.pos > rhs.pos) { + return true; + } + } + return false; + } + + public static bool operator==(Marker lhs, Marker rhs) { + if ((lhs.line.line_no == rhs.line.line_no) && (lhs.pos == rhs.pos)) { + return true; + } + return false; + } + + public static bool operator!=(Marker lhs, Marker rhs) { + if ((lhs.line.line_no != rhs.line.line_no) || (lhs.pos != rhs.pos)) { + return true; + } + return false; + } + + public void Combine(Line move_to_line, int move_to_line_length) { + line = move_to_line; + pos += move_to_line_length; + tag = LineTag.FindTag(line, pos); + } + + // This is for future use, right now Document.Split does it by hand, with some added shortcut logic + public void Split(Line move_to_line, int split_at) { + line = move_to_line; + pos -= split_at; + tag = LineTag.FindTag(line, pos); + } + + public override bool Equals(object obj) { + return this==(Marker)obj; + } + + public override int GetHashCode() { + return base.GetHashCode (); + } + + public override string ToString() { + return "Marker Line " + line + ", Position " + pos; + } + + } + #endregion Structures + + #region Local Variables + private Line document; + private int lines; + private Line sentinel; + private int document_id; + private Random random = new Random(); + internal string password_char; + private StringBuilder password_cache; + private bool calc_pass; + private int char_count; + private bool enable_links; + + // For calculating widths/heights + public static readonly StringFormat string_format = new StringFormat (StringFormat.GenericTypographic); + + private int recalc_suspended; + private bool recalc_pending; + private int recalc_start = 1; // This starts at one, since lines are 1 based + private int recalc_end; + private bool recalc_optimize; + + private int update_suspended; + private bool update_pending; + private int update_start = 1; + + internal bool multiline; + internal HorizontalAlignment alignment; + internal bool wrap; + + internal UndoManager undo; + + internal Marker caret; + internal Marker selection_start; + internal Marker selection_end; + internal bool selection_visible; + internal Marker selection_anchor; + internal Marker selection_prev; + internal bool selection_end_anchor; + + internal int viewport_x; + internal int viewport_y; // The visible area of the document + internal int offset_x; + internal int offset_y; + internal int viewport_width; + internal int viewport_height; + + internal int document_x; // Width of the document + internal int document_y; // Height of the document + + internal int crlf_size; // 1 or 2, depending on whether we use \r\n or just \n + + internal TextBoxBase owner; // Who's owning us? + static internal int caret_width = 1; + static internal int caret_shift = 1; + + internal int left_margin = 2; // A left margin for all lines + internal int top_margin = 2; + internal int right_margin = 2; + #endregion // Local Variables + + #region Constructors + internal Document (TextBoxBase owner) + { + lines = 0; + + this.owner = owner; + + multiline = true; + password_char = ""; + calc_pass = false; + recalc_pending = false; + + // Tree related stuff + sentinel = new Line (this, LineEnding.None); + sentinel.color = LineColor.Black; + + document = sentinel; + + // We always have a blank line + owner.HandleCreated += new EventHandler(owner_HandleCreated); + owner.VisibleChanged += new EventHandler(owner_VisibleChanged); + + Add (1, String.Empty, owner.Font, owner.ForeColor, LineEnding.None); + + undo = new UndoManager (this); + + selection_visible = false; + selection_start.line = this.document; + selection_start.pos = 0; + selection_start.tag = selection_start.line.tags; + selection_end.line = this.document; + selection_end.pos = 0; + selection_end.tag = selection_end.line.tags; + selection_anchor.line = this.document; + selection_anchor.pos = 0; + selection_anchor.tag = selection_anchor.line.tags; + caret.line = this.document; + caret.pos = 0; + caret.tag = caret.line.tags; + + viewport_x = 0; + viewport_y = 0; + + offset_x = 0; + offset_y = 0; + + crlf_size = 2; + + // Default selection is empty + + document_id = random.Next(); + + string_format.Trimming = StringTrimming.None; + string_format.FormatFlags = StringFormatFlags.DisplayFormatControl; + + UpdateMargins (); + } + #endregion + + #region Internal Properties + internal Line Root { + get { + return document; + } + + set { + document = value; + } + } + + // UIA: Method used via reflection in TextRangeProvider + internal int Lines { + get { + return lines; + } + } + + internal Line CaretLine { + get { + return caret.line; + } + } + + internal int CaretPosition { + get { + return caret.pos; + } + } + + internal Point Caret { + get { + return new Point((int)caret.tag.Line.widths[caret.pos] + caret.line.X, caret.line.Y); + } + } + + internal LineTag CaretTag { + get { + return caret.tag; + } + + set { + caret.tag = value; + } + } + + internal int CRLFSize { + get { + return crlf_size; + } + + set { + crlf_size = value; + } + } + + /// <summary> + /// Whether text is scanned for links + /// </summary> + internal bool EnableLinks { + get { return enable_links; } + set { enable_links = value; } + } + + internal string PasswordChar { + get { + return password_char; + } + + set { + password_char = value; + PasswordCache.Length = 0; + if ((password_char.Length != 0) && (password_char[0] != '\0')) { + calc_pass = true; + } else { + calc_pass = false; + } + } + } + + private StringBuilder PasswordCache { + get { + if (password_cache == null) + password_cache = new StringBuilder(); + return password_cache; + } + } + + internal int ViewPortX { + get { + return viewport_x; + } + + set { + viewport_x = value; + } + } + + internal int Length { + get { + return char_count + lines - 1; // Add \n for each line but the last + } + } + + private int CharCount { + get { + return char_count; + } + + set { + char_count = value; + + if (LengthChanged != null) { + LengthChanged(this, EventArgs.Empty); + } + } + } + + internal int ViewPortY { + get { + return viewport_y; + } + + set { + viewport_y = value; + } + } + + internal int OffsetX + { + get + { + return offset_x; + } + + set + { + offset_x = value; + } + } + + internal int OffsetY + { + get + { + return offset_y; + } + + set + { + offset_y = value; + } + } + + internal int ViewPortWidth { + get { + return viewport_width; + } + + set { + viewport_width = value; + } + } + + internal int ViewPortHeight { + get { + return viewport_height; + } + + set { + viewport_height = value; + } + } + + + internal int Width { + get { + return this.document_x; + } + } + + internal int Height { + get { + return this.document_y; + } + } + + internal bool SelectionVisible { + get { + return selection_visible; + } + } + + internal bool Wrap { + get { + return wrap; + } + + set { + wrap = value; + } + } + + #endregion // Internal Properties + + #region Private Methods + + internal void UpdateMargins () + { + switch (owner.actual_border_style) { + case BorderStyle.None: + left_margin = 0; + top_margin = 0; + right_margin = 1; + break; + case BorderStyle.FixedSingle: + left_margin = 2; + top_margin = 2; + right_margin = 3; + break; + case BorderStyle.Fixed3D: + left_margin = 1; + top_margin = 1; + right_margin = 2; + break; + } + } + + internal void SuspendRecalc () + { + if (recalc_suspended == 0) { + recalc_start = int.MaxValue; + recalc_end = int.MinValue; + } + + recalc_suspended++; + } + + internal void ResumeRecalc (bool immediate_update) + { + if (recalc_suspended > 0) + recalc_suspended--; + + if (recalc_suspended == 0 && (immediate_update || recalc_pending) && !(recalc_start == int.MaxValue && recalc_end == int.MinValue)) { + RecalculateDocument (owner.CreateGraphicsInternal (), recalc_start, recalc_end, recalc_optimize); + recalc_pending = false; + } + } + + internal void SuspendUpdate () + { + update_suspended++; + } + + internal void ResumeUpdate (bool immediate_update) + { + if (update_suspended > 0) + update_suspended--; + + if (immediate_update && update_suspended == 0 && update_pending) { + UpdateView (GetLine (update_start), 0); + update_pending = false; + } + } + + // For debugging + internal int DumpTree(Line line, bool with_tags) { + int total; + + total = 1; + + Console.Write("Line {0} [# {1}], Y: {2}, ending style: {3}, Text: '{4}'", + line.line_no, line.GetHashCode(), line.Y, line.ending, + line.text != null ? line.text.ToString() : "undefined"); + + if (line.left == sentinel) { + Console.Write(", left = sentinel"); + } else if (line.left == null) { + Console.Write(", left = NULL"); + } + + if (line.right == sentinel) { + Console.Write(", right = sentinel"); + } else if (line.right == null) { + Console.Write(", right = NULL"); + } + + Console.WriteLine(""); + + if (with_tags) { + LineTag tag; + int count; + int length; + + tag = line.tags; + count = 1; + length = 0; + Console.Write(" Tags: "); + while (tag != null) { + Console.Write("{0} <{1}>-<{2}>", count++, tag.Start, tag.End + /*line.text.ToString (tag.start - 1, tag.length)*/); + length += tag.Length; + + if (tag.Line != line) { + Console.Write("BAD line link"); + throw new Exception("Bad line link in tree"); + } + tag = tag.Next; + if (tag != null) { + Console.Write(", "); + } + } + if (length > line.text.Length) { + throw new Exception(String.Format("Length of tags more than length of text on line (expected {0} calculated {1})", line.text.Length, length)); + } else if (length < line.text.Length) { + throw new Exception(String.Format("Length of tags less than length of text on line (expected {0} calculated {1})", line.text.Length, length)); + } + Console.WriteLine(""); + } + if (line.left != null) { + if (line.left != sentinel) { + total += DumpTree(line.left, with_tags); + } + } else { + if (line != sentinel) { + throw new Exception("Left should not be NULL"); + } + } + + if (line.right != null) { + if (line.right != sentinel) { + total += DumpTree(line.right, with_tags); + } + } else { + if (line != sentinel) { + throw new Exception("Right should not be NULL"); + } + } + + for (int i = 1; i <= this.lines; i++) { + if (GetLine(i) == null) { + throw new Exception(String.Format("Hole in line order, missing {0}", i)); + } + } + + if (line == this.Root) { + if (total < this.lines) { + throw new Exception(String.Format("Not enough nodes in tree, found {0}, expected {1}", total, this.lines)); + } else if (total > this.lines) { + throw new Exception(String.Format("Too many nodes in tree, found {0}, expected {1}", total, this.lines)); + } + } + + return total; + } + + private void SetSelectionVisible (bool value) + { + bool old_selection_visible = selection_visible; + selection_visible = value; + + // cursor and selection are enemies, we can't have both in the same room at the same time + if (owner.IsHandleCreated && !owner.show_caret_w_selection) + XplatUI.CaretVisible (owner.Handle, !selection_visible); + if (UIASelectionChanged != null && (selection_visible || old_selection_visible)) + UIASelectionChanged (this, EventArgs.Empty); + } + + private void DecrementLines(int line_no) { + int current; + + current = line_no; + while (current <= lines) { + GetLine(current).line_no--; + current++; + } + return; + } + + private void IncrementLines(int line_no) { + int current; + + current = this.lines; + while (current >= line_no) { + GetLine(current).line_no++; + current--; + } + return; + } + + private void RebalanceAfterAdd(Line line1) { + Line line2; + + while ((line1 != document) && (line1.parent.color == LineColor.Red)) { + if (line1.parent == line1.parent.parent.left) { + line2 = line1.parent.parent.right; + + if ((line2 != null) && (line2.color == LineColor.Red)) { + line1.parent.color = LineColor.Black; + line2.color = LineColor.Black; + line1.parent.parent.color = LineColor.Red; + line1 = line1.parent.parent; + } else { + if (line1 == line1.parent.right) { + line1 = line1.parent; + RotateLeft(line1); + } + + line1.parent.color = LineColor.Black; + line1.parent.parent.color = LineColor.Red; + + RotateRight(line1.parent.parent); + } + } else { + line2 = line1.parent.parent.left; + + if ((line2 != null) && (line2.color == LineColor.Red)) { + line1.parent.color = LineColor.Black; + line2.color = LineColor.Black; + line1.parent.parent.color = LineColor.Red; + line1 = line1.parent.parent; + } else { + if (line1 == line1.parent.left) { + line1 = line1.parent; + RotateRight(line1); + } + + line1.parent.color = LineColor.Black; + line1.parent.parent.color = LineColor.Red; + RotateLeft(line1.parent.parent); + } + } + } + document.color = LineColor.Black; + } + + private void RebalanceAfterDelete(Line line1) { + Line line2; + + while ((line1 != document) && (line1.color == LineColor.Black)) { + if (line1 == line1.parent.left) { + line2 = line1.parent.right; + if (line2.color == LineColor.Red) { + line2.color = LineColor.Black; + line1.parent.color = LineColor.Red; + RotateLeft(line1.parent); + line2 = line1.parent.right; + } + if ((line2.left.color == LineColor.Black) && (line2.right.color == LineColor.Black)) { + line2.color = LineColor.Red; + line1 = line1.parent; + } else { + if (line2.right.color == LineColor.Black) { + line2.left.color = LineColor.Black; + line2.color = LineColor.Red; + RotateRight(line2); + line2 = line1.parent.right; + } + line2.color = line1.parent.color; + line1.parent.color = LineColor.Black; + line2.right.color = LineColor.Black; + RotateLeft(line1.parent); + line1 = document; + } + } else { + line2 = line1.parent.left; + if (line2.color == LineColor.Red) { + line2.color = LineColor.Black; + line1.parent.color = LineColor.Red; + RotateRight(line1.parent); + line2 = line1.parent.left; + } + if ((line2.right.color == LineColor.Black) && (line2.left.color == LineColor.Black)) { + line2.color = LineColor.Red; + line1 = line1.parent; + } else { + if (line2.left.color == LineColor.Black) { + line2.right.color = LineColor.Black; + line2.color = LineColor.Red; + RotateLeft(line2); + line2 = line1.parent.left; + } + line2.color = line1.parent.color; + line1.parent.color = LineColor.Black; + line2.left.color = LineColor.Black; + RotateRight(line1.parent); + line1 = document; + } + } + } + line1.color = LineColor.Black; + } + + private void RotateLeft(Line line1) { + Line line2 = line1.right; + + line1.right = line2.left; + + if (line2.left != sentinel) { + line2.left.parent = line1; + } + + if (line2 != sentinel) { + line2.parent = line1.parent; + } + + if (line1.parent != null) { + if (line1 == line1.parent.left) { + line1.parent.left = line2; + } else { + line1.parent.right = line2; + } + } else { + document = line2; + } + + line2.left = line1; + if (line1 != sentinel) { + line1.parent = line2; + } + } + + private void RotateRight(Line line1) { + Line line2 = line1.left; + + line1.left = line2.right; + + if (line2.right != sentinel) { + line2.right.parent = line1; + } + + if (line2 != sentinel) { + line2.parent = line1.parent; + } + + if (line1.parent != null) { + if (line1 == line1.parent.right) { + line1.parent.right = line2; + } else { + line1.parent.left = line2; + } + } else { + document = line2; + } + + line2.right = line1; + if (line1 != sentinel) { + line1.parent = line2; + } + } + + + internal void UpdateView(Line line, int pos) { + if (!owner.IsHandleCreated) { + return; + } + + if (update_suspended > 0) { + update_start = Math.Min (update_start, line.line_no); + // update_end = Math.Max (update_end, line.line_no); + // recalc_optimize = true; + update_pending = true; + return; + } + + // Optimize invalidation based on Line alignment + if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no, true)) { + // Lineheight changed, invalidate the rest of the document + if ((line.Y - viewport_y) >=0 ) { + // We formatted something that's in view, only draw parts of the screen + owner.Invalidate(new Rectangle( + offset_x, + line.Y - viewport_y + offset_y, + viewport_width, + owner.Height - (line.Y - viewport_y))); + } else { + // The tag was above the visible area, draw everything + owner.Invalidate(); + } + } else { + switch(line.alignment) { + case HorizontalAlignment.Left: { + owner.Invalidate(new Rectangle( + line.X + ((int)line.widths[pos] - viewport_x - 1) + offset_x, + line.Y - viewport_y + offset_y, + viewport_width, + line.height + 1)); + break; + } + + case HorizontalAlignment.Center: { + owner.Invalidate(new Rectangle( + line.X + offset_x, + line.Y - viewport_y + offset_y, + viewport_width, + line.height + 1)); + break; + } + + case HorizontalAlignment.Right: { + owner.Invalidate(new Rectangle( + line.X + offset_x, + line.Y - viewport_y + offset_y, + (int)line.widths[pos + 1] - viewport_x + line.X, + line.height + 1)); + break; + } + } + } + } + + + // Update display from line, down line_count lines; pos is unused, but required for the signature + internal void UpdateView(Line line, int line_count, int pos) { + if (!owner.IsHandleCreated) { + return; + } + + if (recalc_suspended > 0) { + recalc_start = Math.Min (recalc_start, line.line_no); + recalc_end = Math.Max (recalc_end, line.line_no + line_count); + recalc_optimize = true; + recalc_pending = true; + return; + } + + int start_line_top = line.Y; + + Line end_line = GetLine (line.line_no + line_count); + if (end_line == null) + end_line = GetLine (lines); + + if (end_line == null) + return; + + int end_line_bottom = end_line.Y + end_line.height; + + if (RecalculateDocument(owner.CreateGraphicsInternal(), line.line_no, line.line_no + line_count, true)) { + // Lineheight changed, invalidate the rest of the document + if ((line.Y - viewport_y) >=0 ) { + // We formatted something that's in view, only draw parts of the screen + owner.Invalidate(new Rectangle( + offset_x, + line.Y - viewport_y + offset_y, + viewport_width, + owner.Height - (line.Y - viewport_y))); + } else { + // The tag was above the visible area, draw everything + owner.Invalidate(); + } + } else { + int x = 0 - viewport_x + offset_x; + int w = viewport_width; + int y = Math.Min (start_line_top - viewport_y, line.Y - viewport_y) + offset_y; + int h = Math.Max (end_line_bottom - y, end_line.Y + end_line.height - y); + + owner.Invalidate (new Rectangle (x, y, w, h)); + } + } + + /// <summary> + /// Scans the next paragraph for http:/ ftp:/ www. https:/ etc and marks the tags + /// as links. + /// </summary> + /// <param name="start_line">The line to start on</param> + /// <param name="link_changed">marks as true if something is changed</param> + private void ScanForLinks (Line start_line, ref bool link_changed) + { + Line current_line = start_line; + StringBuilder line_no_breaks = new StringBuilder (); + StringBuilder line_link_record = new StringBuilder (); + ArrayList cumulative_length_list = new ArrayList (); + bool update_caret_tag = false; + + cumulative_length_list.Add (0); + + while (current_line != null) { + line_no_breaks.Append (current_line.text); + + if (link_changed == false) + current_line.LinkRecord (line_link_record); + + current_line.ClearLinks (); + + cumulative_length_list.Add (line_no_breaks.Length); + + if (current_line.ending == LineEnding.Wrap) + current_line = GetLine (current_line.LineNo + 1); + else + break; + } + + // search for protocols.. make sure www. is first! + string [] search_terms = new string [] { "www.", "http:/", "ftp:/", "https:/" }; + int search_found = 0; + int index_found = 0; + string line_no_breaks_string = line_no_breaks.ToString (); + int line_no_breaks_index = 0; + int link_end = 0; + + while (true) { + if (line_no_breaks_index >= line_no_breaks_string.Length) + break; + + index_found = FirstIndexOfAny (line_no_breaks_string, search_terms, line_no_breaks_index, out search_found); + + //no links found on this line + if (index_found == -1) + break; + + if (search_found == 0) { + // if we are at the end of the line to analyse and the end of the line + // is "www." then there are no links here + if (line_no_breaks_string.Length == index_found + search_terms [0].Length) + break; + + // if after www. we don't have a letter a digit or a @ or - or / + // then it is not a web address, we should continue searching + if (char.IsLetterOrDigit (line_no_breaks_string [index_found + search_terms [0].Length]) == false && + "@/~".IndexOf (line_no_breaks_string [index_found + search_terms [0].Length].ToString ()) == -1) { + line_no_breaks_index = index_found + search_terms [0].Length; + continue; + } + } + + link_end = line_no_breaks_string.Length - 1; + line_no_breaks_index = line_no_breaks_string.Length; + + // we've found a link, we just need to find where it ends now + for (int i = index_found + search_terms [search_found].Length; i < line_no_breaks_string.Length; i++) { + if (line_no_breaks_string [i - 1] == '.') { + if (char.IsLetterOrDigit (line_no_breaks_string [i]) == false && + "@/~".IndexOf (line_no_breaks_string [i].ToString ()) == -1) { + link_end = i - 1; + line_no_breaks_index = i; + break; + } + } else { + if (char.IsLetterOrDigit (line_no_breaks_string [i]) == false && + "@-/:~.?=_&".IndexOf (line_no_breaks_string [i].ToString ()) == -1) { + link_end = i - 1; + line_no_breaks_index = i; + break; + } + } + } + + string link_text = line_no_breaks_string.Substring (index_found, link_end - index_found + 1); + int current_cumulative = 0; + + // we've found a link - index_found -> link_end + // now we just make all the tags as containing link and + // point them to the text for the whole link + + current_line = start_line; + + //find the line we start on + for (current_cumulative = 1; current_cumulative < cumulative_length_list.Count; current_cumulative++) + if ((int)cumulative_length_list [current_cumulative] > index_found) + break; + + current_line = GetLine (start_line.LineNo + current_cumulative - 1); + + // find the tag we start on + LineTag current_tag = current_line.FindTag (index_found - (int)cumulative_length_list [current_cumulative - 1] + 1); + + if (current_tag.Start != (index_found - (int)cumulative_length_list [current_cumulative - 1]) + 1) { + if (current_tag == CaretTag) + update_caret_tag = true; + + current_tag = current_tag.Break ((index_found - (int)cumulative_length_list [current_cumulative - 1]) + 1); + } + + // set the tag + current_tag.IsLink = true; + current_tag.LinkText = link_text; + + //go through each character + // find the tag we are in + // skip the number of characters in the tag + for (int i = 1; i < link_text.Length; i++) { + // on to a new word-wrapped line + if ((int)cumulative_length_list [current_cumulative] <= index_found + i) { + + current_line = GetLine (start_line.LineNo + current_cumulative++); + current_tag = current_line.FindTag (index_found + i - (int)cumulative_length_list [current_cumulative - 1] + 1); + + current_tag.IsLink = true; + current_tag.LinkText = link_text; + + continue; + } + + if (current_tag.End < index_found + 1 + i - (int)cumulative_length_list [current_cumulative - 1]) { + // skip empty tags in the middle of the URL + do { + current_tag = current_tag.Next; + } while (current_tag.Length == 0); + + current_tag.IsLink = true; + current_tag.LinkText = link_text; + } + } + + //if there are characters left in the tag after the link + // split the tag + // make the second part a non link + if (current_tag.End > (index_found + link_text.Length + 1) - (int)cumulative_length_list [current_cumulative - 1]) { + if (current_tag == CaretTag) + update_caret_tag = true; + + current_tag.Break ((index_found + link_text.Length + 1) - (int)cumulative_length_list [current_cumulative - 1]); + } + } + + if (update_caret_tag) { + CaretTag = LineTag.FindTag (CaretLine, CaretPosition); + link_changed = true; + } else { + if (link_changed == false) { + current_line = start_line; + StringBuilder new_link_record = new StringBuilder (); + + while (current_line != null) { + current_line.LinkRecord (new_link_record); + + if (current_line.ending == LineEnding.Wrap) + current_line = GetLine (current_line.LineNo + 1); + else + break; + } + + if (new_link_record.Equals (line_link_record) == false) + link_changed = true; + } + } + } + + private int FirstIndexOfAny (string haystack, string [] needles, int start_index, out int term_found) + { + term_found = -1; + int best_index = -1; + + for (int i = 0; i < needles.Length; i++) { + int index = haystack.IndexOf (needles [i], start_index, StringComparison.InvariantCultureIgnoreCase); + + if (index > -1) { + if (term_found > -1) { + if (index < best_index) { + best_index = index; + term_found = i; + } + } else { + best_index = index; + term_found = i; + } + } + } + + return best_index; + } + + + + private void InvalidateLinks (Rectangle clip) + { + for (int i = (owner.list_links.Count - 1); i >= 0; i--) { + TextBoxBase.LinkRectangle link = (TextBoxBase.LinkRectangle) owner.list_links [i]; + + if (clip.IntersectsWith (link.LinkAreaRectangle)) + owner.list_links.RemoveAt (i); + } + } + #endregion // Private Methods + + #region Internal Methods + + internal void ScanForLinks (int start, int end, ref bool link_changed) + { + Line line = null; + LineEnding lastending = LineEnding.Rich; + + // make sure we start scanning at the real begining of the line + while (true) { + if (start != 1 && GetLine (start - 1).ending == LineEnding.Wrap) + start--; + else + break; + } + + for (int i = start; i <= end && i <= lines; i++) { + line = GetLine (i); + + if (lastending != LineEnding.Wrap) + ScanForLinks (line, ref link_changed); + + lastending = line.ending; + + if (lastending == LineEnding.Wrap && (i + 1) <= end) + end++; + } + } + + // Clear the document and reset state + internal void Empty() { + + document = sentinel; + lines = 0; + + // We always have a blank line + Add (1, String.Empty, owner.Font, owner.ForeColor, LineEnding.None); + + this.RecalculateDocument(owner.CreateGraphicsInternal()); + PositionCaret(0, 0); + + SetSelectionVisible (false); + + selection_start.line = this.document; + selection_start.pos = 0; + selection_start.tag = selection_start.line.tags; + selection_end.line = this.document; + selection_end.pos = 0; + selection_end.tag = selection_end.line.tags; + char_count = 0; + + viewport_x = 0; + viewport_y = 0; + + document_x = 0; + document_y = 0; + + if (owner.IsHandleCreated) + owner.Invalidate (); + } + + internal void PositionCaret(Line line, int pos) { + caret.tag = line.FindTag (pos); + + MoveCaretToTextTag (); + + caret.line = line; + caret.pos = pos; + + if (owner.IsHandleCreated) { + if (owner.Focused) { + if (caret.height != caret.tag.Height) + XplatUI.CreateCaret (owner.Handle, caret_width, caret.height); + XplatUI.SetCaretPos(owner.Handle, + offset_x + (int)caret.tag.Line.widths[caret.pos] + caret.line.X - viewport_x, + offset_y + caret.line.Y + caret.tag.Shift - viewport_y + caret_shift); + } + + if (CaretMoved != null) CaretMoved(this, EventArgs.Empty); + } + + // We set this at the end because we use the heights to determine whether or + // not we need to recreate the caret + caret.height = caret.tag.Height; + + } + + internal void PositionCaret(int x, int y) { + if (!owner.IsHandleCreated) { + return; + } + + caret.tag = FindCursor(x, y, out caret.pos); + + MoveCaretToTextTag (); + + caret.line = caret.tag.Line; + caret.height = caret.tag.Height; + + if (owner.ShowSelection && (!selection_visible || owner.show_caret_w_selection)) { + XplatUI.CreateCaret (owner.Handle, caret_width, caret.height); + XplatUI.SetCaretPos(owner.Handle, + (int)caret.tag.Line.widths[caret.pos] + caret.line.X - viewport_x + offset_x, + offset_y + caret.line.Y + caret.tag.Shift - viewport_y + caret_shift); + } + + if (CaretMoved != null) CaretMoved(this, EventArgs.Empty); + } + + internal void CaretHasFocus() { + if ((caret.tag != null) && owner.IsHandleCreated) { + XplatUI.CreateCaret(owner.Handle, caret_width, caret.height); + XplatUI.SetCaretPos(owner.Handle, + offset_x + (int)caret.tag.Line.widths[caret.pos] + caret.line.X - viewport_x, + offset_y + caret.line.Y + caret.tag.Shift - viewport_y + caret_shift); + + DisplayCaret (); + } + + if (owner.IsHandleCreated && SelectionLength () > 0) { + InvalidateSelectionArea (); + } + } + + internal void CaretLostFocus() { + if (!owner.IsHandleCreated) { + return; + } + XplatUI.DestroyCaret(owner.Handle); + } + + internal void AlignCaret () + { + AlignCaret (true); + } + + internal void AlignCaret(bool changeCaretTag) { + if (!owner.IsHandleCreated) { + return; + } + + if (changeCaretTag) { + caret.tag = LineTag.FindTag (caret.line, caret.pos); + + MoveCaretToTextTag (); + } + + // if the caret has had SelectionFont changed to a + // different height, we reflect changes unless the new + // font is larger than the line (line recalculations + // ignore empty tags) in which case we make it equal + // the line height and then when text is entered + if (caret.tag.Height > caret.tag.Line.Height) { + caret.height = caret.line.height; + } else { + caret.height = caret.tag.Height; + } + + if (owner.Focused) { + XplatUI.CreateCaret(owner.Handle, caret_width, caret.height); + XplatUI.SetCaretPos (owner.Handle, + offset_x + (int) caret.tag.Line.widths [caret.pos] + caret.line.X - viewport_x, + offset_y + caret.line.Y + viewport_y + caret_shift); + DisplayCaret (); + } + + if (CaretMoved != null) CaretMoved(this, EventArgs.Empty); + } + + internal void UpdateCaret() { + if (!owner.IsHandleCreated || caret.tag == null) { + return; + } + + MoveCaretToTextTag (); + + if (caret.tag.Height != caret.height) { + caret.height = caret.tag.Height; + if (owner.Focused) { + XplatUI.CreateCaret(owner.Handle, caret_width, caret.height); + } + } + + if (owner.Focused) { + XplatUI.SetCaretPos(owner.Handle, + offset_x + (int)caret.tag.Line.widths[caret.pos] + caret.line.X - viewport_x, + offset_y + caret.line.Y + caret.tag.Shift - viewport_y + caret_shift); + DisplayCaret (); + } + + if (CaretMoved != null) CaretMoved(this, EventArgs.Empty); + } + + internal void DisplayCaret() { + if (!owner.IsHandleCreated) { + return; + } + + if (owner.ShowSelection && (!selection_visible || owner.show_caret_w_selection)) { + XplatUI.CaretVisible(owner.Handle, true); + } + } + + internal void HideCaret() { + if (!owner.IsHandleCreated) { + return; + } + + if (owner.Focused) { + XplatUI.CaretVisible(owner.Handle, false); + } + } + + + internal void MoveCaretToTextTag () + { + if (caret.tag == null || caret.tag.IsTextTag) + return; + + + + if (caret.pos < caret.tag.Start) { + caret.tag = caret.tag.Previous; + } else { + caret.tag = caret.tag.Next; + } + } + + internal void MoveCaret(CaretDirection direction) { + // FIXME should we use IsWordSeparator to detect whitespace, instead + // of looking for actual spaces in the Word move cases? + + bool nowrap = false; + switch(direction) { + case CaretDirection.CharForwardNoWrap: + nowrap = true; + goto case CaretDirection.CharForward; + case CaretDirection.CharForward: { + caret.pos++; + if (caret.pos > caret.line.TextLengthWithoutEnding ()) { + if (!nowrap) { + // Go into next line + if (caret.line.line_no < this.lines) { + caret.line = GetLine(caret.line.line_no+1); + caret.pos = 0; + caret.tag = caret.line.tags; + } else { + caret.pos--; + } + } else { + // Single line; we stay where we are + caret.pos--; + } + } else { + if ((caret.tag.Start - 1 + caret.tag.Length) < caret.pos) { + caret.tag = caret.tag.Next; + } + } + UpdateCaret(); + return; + } + + case CaretDirection.CharBackNoWrap: + nowrap = true; + goto case CaretDirection.CharBack; + case CaretDirection.CharBack: { + if (caret.pos > 0) { + // caret.pos--; // folded into the if below + + if (--caret.pos > 0) { + if (caret.tag.Start > caret.pos) { + caret.tag = caret.tag.Previous; + } + } + } else { + if (caret.line.line_no > 1 && !nowrap) { + caret.line = GetLine(caret.line.line_no - 1); + caret.pos = caret.line.TextLengthWithoutEnding (); + caret.tag = LineTag.FindTag(caret.line, caret.pos); + } + } + UpdateCaret(); + return; + } + + case CaretDirection.WordForward: { + int len; + + len = caret.line.text.Length; + if (caret.pos < len) { + while ((caret.pos < len) && (caret.line.text[caret.pos] != ' ')) { + caret.pos++; + } + if (caret.pos < len) { + // Skip any whitespace + while ((caret.pos < len) && (caret.line.text[caret.pos] == ' ')) { + caret.pos++; + } + } + caret.tag = LineTag.FindTag(caret.line, caret.pos); + } else { + if (caret.line.line_no < this.lines) { + caret.line = GetLine(caret.line.line_no + 1); + caret.pos = 0; + caret.tag = caret.line.tags; + } + } + UpdateCaret(); + return; + } + + case CaretDirection.WordBack: { + if (caret.pos > 0) { + caret.pos--; + + while ((caret.pos > 0) && (caret.line.text[caret.pos] == ' ')) { + caret.pos--; + } + + while ((caret.pos > 0) && (caret.line.text[caret.pos] != ' ')) { + caret.pos--; + } + + if (caret.line.text.ToString(caret.pos, 1) == " ") { + if (caret.pos != 0) { + caret.pos++; + } else { + caret.line = GetLine(caret.line.line_no - 1); + caret.pos = caret.line.text.Length; + } + } + caret.tag = LineTag.FindTag(caret.line, caret.pos); + } else { + if (caret.line.line_no > 1) { + caret.line = GetLine(caret.line.line_no - 1); + caret.pos = caret.line.text.Length; + caret.tag = LineTag.FindTag(caret.line, caret.pos); + } + } + UpdateCaret(); + return; + } + + case CaretDirection.LineUp: { + if (caret.line.line_no > 1) { + int pixel; + + pixel = (int)caret.line.widths[caret.pos]; + PositionCaret(pixel, GetLine(caret.line.line_no - 1).Y); + + DisplayCaret (); + } + return; + } + + case CaretDirection.LineDown: { + if (caret.line.line_no < lines) { + int pixel; + + pixel = (int)caret.line.widths[caret.pos]; + PositionCaret(pixel, GetLine(caret.line.line_no + 1).Y); + + DisplayCaret (); + } + return; + } + + case CaretDirection.Home: { + if (caret.pos > 0) { + caret.pos = 0; + caret.tag = caret.line.tags; + UpdateCaret(); + } + return; + } + + case CaretDirection.End: { + if (caret.pos < caret.line.TextLengthWithoutEnding ()) { + caret.pos = caret.line.TextLengthWithoutEnding (); + caret.tag = LineTag.FindTag(caret.line, caret.pos); + UpdateCaret(); + } + return; + } + + case CaretDirection.PgUp: { + + if (caret.line.line_no == 1 && owner.richtext) { + owner.vscroll.Value = 0; + Line line = GetLine (1); + PositionCaret (line, 0); + } + + int y_offset = caret.line.Y + caret.line.height - 1 - viewport_y; + int index; + LineTag top = FindCursor ((int) caret.line.widths [caret.pos], + viewport_y - viewport_height, out index); + + owner.vscroll.Value = Math.Min (top.Line.Y, owner.vscroll.Maximum - viewport_height); + PositionCaret ((int) caret.line.widths [caret.pos], y_offset + viewport_y); + + return; + } + + case CaretDirection.PgDn: { + + if (caret.line.line_no == lines && owner.richtext) { + owner.vscroll.Value = owner.vscroll.Maximum - viewport_height + 1; + Line line = GetLine (lines); + PositionCaret (line, line.TextLengthWithoutEnding()); + } + + int y_offset = caret.line.Y - viewport_y; + int index; + LineTag top = FindCursor ((int) caret.line.widths [caret.pos], + viewport_y + viewport_height, out index); + + owner.vscroll.Value = Math.Min (top.Line.Y, owner.vscroll.Maximum - viewport_height); + PositionCaret ((int) caret.line.widths [caret.pos], y_offset + viewport_y); + + return; + } + + case CaretDirection.CtrlPgUp: { + PositionCaret(0, viewport_y); + DisplayCaret (); + return; + } + + case CaretDirection.CtrlPgDn: { + Line line; + LineTag tag; + int index; + + tag = FindCursor (0, viewport_y + viewport_height, out index); + if (tag.Line.line_no > 1) { + line = GetLine(tag.Line.line_no - 1); + } else { + line = tag.Line; + } + PositionCaret(line, line.Text.Length); + DisplayCaret (); + return; + } + + case CaretDirection.CtrlHome: { + caret.line = GetLine(1); + caret.pos = 0; + caret.tag = caret.line.tags; + + UpdateCaret(); + return; + } + + case CaretDirection.CtrlEnd: { + caret.line = GetLine(lines); + caret.pos = caret.line.TextLengthWithoutEnding (); + caret.tag = LineTag.FindTag(caret.line, caret.pos); + + UpdateCaret(); + return; + } + + case CaretDirection.SelectionStart: { + caret.line = selection_start.line; + caret.pos = selection_start.pos; + caret.tag = selection_start.tag; + + UpdateCaret(); + return; + } + + case CaretDirection.SelectionEnd: { + caret.line = selection_end.line; + caret.pos = selection_end.pos; + caret.tag = selection_end.tag; + + UpdateCaret(); + return; + } + } + } + + internal void DumpDoc () + { + Console.WriteLine ("<doc lines='{0}'>", lines); + for (int i = 1; i <= lines ; i++) { + Line line = GetLine (i); + Console.WriteLine ("<line no='{0}' ending='{1}'>", line.line_no, line.ending); + + LineTag tag = line.tags; + while (tag != null) { + Console.Write ("\t<tag type='{0}' span='{1}->{2}' font='{3}' color='{4}'>", + tag.GetType (), tag.Start, tag.Length, tag.Font, tag.Color); + Console.Write (tag.Text ()); + Console.WriteLine ("</tag>"); + tag = tag.Next; + } + Console.WriteLine ("</line>"); + } + Console.WriteLine ("</doc>"); + } + + // UIA: Used via reflection by TextProviderBehavior + internal void GetVisibleLineIndexes (Rectangle clip, out int start, out int end) + { + if (multiline) { + /* Expand the region slightly to be sure to + * paint the full extent of the line of text. + * See bug 464464. + */ + start = GetLineByPixel(clip.Top + viewport_y - offset_y - 1, false).line_no; + end = GetLineByPixel(clip.Bottom + viewport_y - offset_y + 1, false).line_no; + } else { + start = GetLineByPixel(clip.Left + viewport_x - offset_x, false).line_no; + end = GetLineByPixel(clip.Right + viewport_x - offset_x, false).line_no; + } + } + + internal void Draw (Graphics g, Rectangle clip) + { + Line line; // Current line being drawn + LineTag tag; // Current tag being drawn + int start; // First line to draw + int end; // Last line to draw + StringBuilder text; // String representing the current line + int line_no; + Color tag_color; + Color current_color; + + // First, figure out from what line to what line we need to draw + GetVisibleLineIndexes (clip, out start, out end); + + // remove links in the list (used for mouse down events) that are within the clip area. + InvalidateLinks (clip); + + /// + /// We draw the single border ourself + /// + if (owner.actual_border_style == BorderStyle.FixedSingle) { + WidgetPaint.DrawBorder (g, owner.ClientRectangle, Color.Black, ButtonBorderStyle.Solid); + } + + /// Make sure that we aren't drawing one more line then we need to + line = GetLine (end - 1); + if (line != null && clip.Bottom == offset_y + line.Y + line.height - viewport_y) + end--; + + line_no = start; + + #if Debug + DateTime n = DateTime.Now; + Console.WriteLine ("Started drawing: {0}s {1}ms", n.Second, n.Millisecond); + Console.WriteLine ("CLIP: {0}", clip); + Console.WriteLine ("S: {0}", GetLine (start).text); + Console.WriteLine ("E: {0}", GetLine (end).text); + #endif + + // Non multiline selection can be handled outside of the loop + if (!multiline && selection_visible && owner.ShowSelection) { + g.FillRectangle (ThemeEngine.Current.ResPool.GetSolidBrush (ThemeEngine.Current.ColorHighlight), + offset_x + selection_start.line.widths [selection_start.pos] + + selection_start.line.X - viewport_x, + offset_y + selection_start.line.Y, + (selection_end.line.X + selection_end.line.widths [selection_end.pos]) - + (selection_start.line.X + selection_start.line.widths [selection_start.pos]), + selection_start.line.height); + } + + while (line_no <= end) { + line = GetLine (line_no); + float line_y = line.Y - viewport_y + offset_y; + + tag = line.tags; + if (!calc_pass) { + text = line.text; + } else { + if (PasswordCache.Length < line.text.Length) + PasswordCache.Append(Char.Parse(password_char), line.text.Length - PasswordCache.Length); + else if (PasswordCache.Length > line.text.Length) + PasswordCache.Remove(line.text.Length, PasswordCache.Length - line.text.Length); + text = PasswordCache; + } + + int line_selection_start = text.Length + 1; + int line_selection_end = text.Length + 1; + if (selection_visible && owner.ShowSelection && + (line_no >= selection_start.line.line_no) && + (line_no <= selection_end.line.line_no)) { + + if (line_no == selection_start.line.line_no) + line_selection_start = selection_start.pos + 1; + else + line_selection_start = 1; + + if (line_no == selection_end.line.line_no) + line_selection_end = selection_end.pos + 1; + else + line_selection_end = text.Length + 1; + + if (line_selection_end == line_selection_start) { + // There isn't really selection + line_selection_start = text.Length + 1; + line_selection_end = line_selection_start; + } else if (multiline) { + // lets draw some selection baby!! (non multiline selection is drawn outside the loop) + g.FillRectangle (ThemeEngine.Current.ResPool.GetSolidBrush (ThemeEngine.Current.ColorHighlight), + offset_x + line.widths [line_selection_start - 1] + line.X - viewport_x, + line_y, line.widths [line_selection_end - 1] - line.widths [line_selection_start - 1], + line.height); + } + } + + current_color = line.tags.ColorToDisplay; + while (tag != null) { + + // Skip empty tags + if (tag.Length == 0) { + tag = tag.Next; + continue; + } + + if (((tag.X + tag.Width) < (clip.Left - viewport_x - offset_x)) && + (tag.X > (clip.Right - viewport_x - offset_x))) { + tag = tag.Next; + continue; + } + + if (tag.BackColor != Color.Empty) { + g.FillRectangle (ThemeEngine.Current.ResPool.GetSolidBrush (tag.BackColor), + offset_x + tag.X + line.X - viewport_x, + line_y + tag.Shift, tag.Width, line.height); + } + + tag_color = tag.ColorToDisplay; + current_color = tag_color; + + if (!owner.Enabled) { + Color a = tag.Color; + Color b = ThemeEngine.Current.ColorWindowText; + + if ((a.R == b.R) && (a.G == b.G) && (a.B == b.B)) + tag_color = ThemeEngine.Current.ColorGrayText; + + } + + int tag_pos = tag.Start; + current_color = tag_color; + while (tag_pos < tag.Start + tag.Length) { + int old_tag_pos = tag_pos; + + if (tag_pos >= line_selection_start && tag_pos < line_selection_end) { + current_color = ThemeEngine.Current.ColorHighlightText; + tag_pos = Math.Min (tag.End, line_selection_end); + } else if (tag_pos < line_selection_start) { + current_color = tag_color; + tag_pos = Math.Min (tag.End, line_selection_start); + } else { + current_color = tag_color; + tag_pos = tag.End; + } + + Rectangle text_size; + + tag.Draw (g, current_color, + offset_x + line.X - viewport_x, + line_y + tag.Shift, + old_tag_pos - 1, Math.Min (tag.Start + tag.Length, tag_pos) - 1, + text.ToString (), out text_size, tag.IsLink); + + if (tag.IsLink) { + TextBoxBase.LinkRectangle link = new TextBoxBase.LinkRectangle (text_size); + link.LinkTag = tag; + owner.list_links.Add (link); + } + } + tag = tag.Next; + } + + line.DrawEnding (g, line_y); + line_no++; + } + } + + private int GetLineEnding (string line, int start, out LineEnding ending) + { + int res; + int rich_index; + + if (start >= line.Length) { + ending = LineEnding.Wrap; + return -1; + } + + res = line.IndexOf ('\r', start); + rich_index = line.IndexOf ('\n', start); + + // Handle the case where we find both of them, and the \n is before the \r + if (res != -1 && rich_index != -1) + if (rich_index < res) { + ending = LineEnding.Rich; + return rich_index; + } + + if (res != -1) { + if (res + 2 < line.Length && line [res + 1] == '\r' && line [res + 2] == '\n') { + ending = LineEnding.Soft; + return res; + } + if (res + 1 < line.Length && line [res + 1] == '\n') { + ending = LineEnding.Hard; + return res; + } + ending = LineEnding.Limp; + return res; + } + + if (rich_index != -1) { + ending = LineEnding.Rich; + return rich_index; + } + + ending = LineEnding.Wrap; + return line.Length; + } + + // Get the line ending, but only of the types specified + private int GetLineEnding (string line, int start, out LineEnding ending, LineEnding type) + { + int index = start; + int last_length = 0; + + do { + index = GetLineEnding (line, index + last_length, out ending); + last_length = LineEndingLength (ending); + } while + ((ending & type) != ending && index != -1); + + return index == -1 ? line.Length : index; + } + + internal int LineEndingLength (LineEnding ending) + { + switch (ending) { + case LineEnding.Limp: + case LineEnding.Rich: + return 1; + case LineEnding.Hard: + return 2; + case LineEnding.Soft: + return 3; + } + + return 0; + } + + internal string LineEndingToString (LineEnding ending) + { + switch (ending) { + case LineEnding.Limp: + return "\r"; + case LineEnding.Hard: + return "\r\n"; + case LineEnding.Soft: + return "\r\r\n"; + case LineEnding.Rich: + return "\n"; + } + + return string.Empty; + } + + internal LineEnding StringToLineEnding (string ending) + { + switch (ending) { + case "\r": + return LineEnding.Limp; + case "\r\n": + return LineEnding.Hard; + case "\r\r\n": + return LineEnding.Soft; + case "\n": + return LineEnding.Rich; + default: + return LineEnding.None; + } + } + + internal void Insert (Line line, int pos, bool update_caret, string s) + { + Insert (line, pos, update_caret, s, line.FindTag (pos)); + } + + // Insert text at the given position; use formatting at insertion point for inserted text + internal void Insert (Line line, int pos, bool update_caret, string s, LineTag tag) + { + int break_index; + int base_line; + int old_line_count; + int count = 1; + LineEnding ending; + Line split_line; + + // Don't recalculate while we mess around + SuspendRecalc (); + + base_line = line.line_no; + old_line_count = lines; + + // Discard chars after any possible -unlikely- end of file + int eof_index = s.IndexOf ('\0'); + if (eof_index != -1) + s = s.Substring (0, eof_index); + + break_index = GetLineEnding (s, 0, out ending, LineEnding.Hard | LineEnding.Rich); + + // There are no line feeds in our text to be pasted + if (break_index == s.Length) { + line.InsertString (pos, s, tag); + } else { + // Add up to the first line feed to our current position + line.InsertString (pos, s.Substring (0, break_index + LineEndingLength (ending)), tag); + + // Split the rest of the original line to a new line + Split (line, pos + (break_index + LineEndingLength (ending))); + line.ending = ending; + break_index += LineEndingLength (ending); + split_line = GetLine (line.line_no + 1); + + // Insert brand new lines for any more line feeds in the inserted string + while (true) { + int next_break = GetLineEnding (s, break_index, out ending, LineEnding.Hard | LineEnding.Rich); + + if (next_break == s.Length) + break; + + string line_text = s.Substring (break_index, next_break - break_index + + LineEndingLength (ending)); + + Add (base_line + count, line_text, line.alignment, tag.Font, tag.Color, ending); + + Line last = GetLine (base_line + count); + last.ending = ending; + + count++; + break_index = next_break + LineEndingLength (ending); + } + + // Add the remainder of the insert text to the split + // part of the original line + split_line.InsertString (0, s.Substring (break_index)); + } + + // Allow the document to recalculate things + ResumeRecalc (false); + + // Update our character count + CharCount += s.Length; + + UpdateView (line, lines - old_line_count + 1, pos); + + // Move the caret to the end of the inserted text if requested + if (update_caret) { + Line l = GetLine (line.line_no + lines - old_line_count); + PositionCaret (l, l.text.Length); + DisplayCaret (); + } + } + + // Inserts a string at the given position + internal void InsertString (Line line, int pos, string s) + { + // Update our character count + CharCount += s.Length; + + // Insert the text into the Line + line.InsertString (pos, s); + } + + // Inserts a character at the current caret position + internal void InsertCharAtCaret (char ch, bool move_caret) + { + caret.line.InsertString (caret.pos, ch.ToString(), caret.tag); + + // Update our character count + CharCount++; + + undo.RecordTyping (caret.line, caret.pos, ch); + + UpdateView (caret.line, caret.pos); + + if (move_caret) { + caret.pos++; + UpdateCaret (); + SetSelectionToCaret (true); + } + } + + internal void InsertPicture (Line line, int pos, RTF.Picture picture) + { + //LineTag next_tag; + LineTag tag; + int len; + + len = 1; + + // Just a place holder basically + line.text.Insert (pos, "I"); + + PictureTag picture_tag = new PictureTag (line, pos + 1, picture); + + tag = LineTag.FindTag (line, pos); + picture_tag.CopyFormattingFrom (tag); + /*next_tag = */tag.Break (pos + 1); + picture_tag.Previous = tag; + picture_tag.Next = tag.Next; + tag.Next = picture_tag; + + // + // Picture tags need to be surrounded by text tags + // + if (picture_tag.Next == null) { + picture_tag.Next = new LineTag (line, pos + 1); + picture_tag.Next.CopyFormattingFrom (tag); + picture_tag.Next.Previous = picture_tag; + } + + tag = picture_tag.Next; + while (tag != null) { + tag.Start += len; + tag = tag.Next; + } + + line.Grow (len); + line.recalc = true; + + UpdateView (line, pos); + } + + internal void DeleteMultiline (Line start_line, int pos, int length) + { + Marker start = new Marker (); + Marker end = new Marker (); + int start_index = LineTagToCharIndex (start_line, pos); + + start.line = start_line; + start.pos = pos; + start.tag = LineTag.FindTag (start_line, pos); + + CharIndexToLineTag (start_index + length, out end.line, + out end.tag, out end.pos); + + SuspendUpdate (); + + if (start.line == end.line) { + DeleteChars (start.line, pos, end.pos - pos); + } else { + + // Delete first and last lines + DeleteChars (start.line, start.pos, start.line.text.Length - start.pos); + DeleteChars (end.line, 0, end.pos); + + int current = start.line.line_no + 1; + if (current < end.line.line_no) { + for (int i = end.line.line_no - 1; i >= current; i--) { + Delete (i); + } + } + + // BIG FAT WARNING - selection_end.line might be stale due + // to the above Delete() call. DONT USE IT before hitting the end of this method! + + // Join start and end + Combine (start.line.line_no, current); + } + + ResumeUpdate (true); + } + + + // Deletes n characters at the given position; it will not delete past line limits + // pos is 0-based + public void DeleteChars (Line line, int pos, int count) + { + // Reduce our character count + CharCount -= count; + + line.DeleteCharacters (pos, count); + + if (pos >= line.TextLengthWithoutEnding ()) { + LineEnding ending = line.ending; + GetLineEnding (line.text.ToString (), 0, out ending); + + if (ending != line.ending) { + line.ending = ending; + + if (!multiline) { + UpdateView (line, lines, pos); + owner.Invalidate (); + return; + } + } + } + if (!multiline) { + UpdateView (line, lines, pos); + owner.Invalidate (); + } else + UpdateView (line, pos); + } + + // Deletes a character at or after the given position (depending on forward); it will not delete past line limits + public void DeleteChar (Line line, int pos, bool forward) + { + if ((pos == 0 && forward == false) || (pos == line.text.Length && forward == true)) + return; + + undo.BeginUserAction ("Delete"); + + if (forward) { + undo.RecordDeleteString (line, pos, line, pos + 1); + DeleteChars (line, pos, 1); + } else { + undo.RecordDeleteString (line, pos - 1, line, pos); + DeleteChars (line, pos - 1, 1); + } + + undo.EndUserAction (); + } + + // Combine two lines + internal void Combine(int FirstLine, int SecondLine) { + Combine(GetLine(FirstLine), GetLine(SecondLine)); + } + + internal void Combine(Line first, Line second) { + LineTag last; + int shift; + + // strip the ending off of the first lines text + first.text.Length = first.text.Length - LineEndingLength (first.ending); + + // Combine the two tag chains into one + last = first.tags; + + // Maintain the line ending style + first.ending = second.ending; + + while (last.Next != null) { + last = last.Next; + } + + // need to get the shift before setting the next tag since that effects length + shift = last.Start + last.Length - 1; + last.Next = second.tags; + last.Next.Previous = last; + + // Fix up references within the chain + last = last.Next; + while (last != null) { + last.Line = first; + last.Start += shift; + last = last.Next; + } + + // Combine both lines' strings + first.text.Insert(first.text.Length, second.text.ToString()); + first.Grow(first.text.Length); + + // Remove the reference to our (now combined) tags from the doomed line + second.tags = null; + + // Renumber lines + DecrementLines(first.line_no + 2); // first.line_no + 1 will be deleted, so we need to start renumbering one later + + // Mop up + first.recalc = true; + first.height = 0; // This forces RecalcDocument/UpdateView to redraw from this line on + first.Streamline(lines); + + // Update Caret, Selection, etc + if (caret.line == second) { + caret.Combine(first, shift); + } + if (selection_anchor.line == second) { + selection_anchor.Combine(first, shift); + } + if (selection_start.line == second) { + selection_start.Combine(first, shift); + } + if (selection_end.line == second) { + selection_end.Combine(first, shift); + } + + #if Debug + Line check_first; + Line check_second; + + check_first = GetLine(first.line_no); + check_second = GetLine(check_first.line_no + 1); + + Console.WriteLine("Pre-delete: Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y); + #endif + + this.Delete(second); + + #if Debug + check_first = GetLine(first.line_no); + check_second = GetLine(check_first.line_no + 1); + + Console.WriteLine("Post-delete Y of first line: {0}, second line: {1}", check_first.Y, check_second.Y); + #endif + } + + // Split the line at the position into two + internal void Split(int LineNo, int pos) { + Line line; + LineTag tag; + + line = GetLine(LineNo); + tag = LineTag.FindTag(line, pos); + Split(line, tag, pos); + } + + internal void Split(Line line, int pos) { + LineTag tag; + + tag = LineTag.FindTag(line, pos); + Split(line, tag, pos); + } + + ///<summary>Split line at given tag and position into two lines</summary> + ///if more space becomes available on previous line + internal void Split(Line line, LineTag tag, int pos) { + LineTag new_tag; + Line new_line; + bool move_caret; + bool move_sel_start; + bool move_sel_end; + + move_caret = false; + move_sel_start = false; + move_sel_end = false; + +#if DEBUG + SanityCheck(); + + if (tag.End < pos) + throw new Exception ("Split called with the wrong tag"); +#endif + + // Adjust selection and cursors + if (caret.line == line && caret.pos >= pos) { + move_caret = true; + } + if (selection_start.line == line && selection_start.pos > pos) { + move_sel_start = true; + } + + if (selection_end.line == line && selection_end.pos > pos) { + move_sel_end = true; + } + + // cover the easy case first + if (pos == line.text.Length) { + Add (line.line_no + 1, String.Empty, line.alignment, tag.Font, tag.Color, line.ending); + + new_line = GetLine (line.line_no + 1); + + if (move_caret) { + caret.line = new_line; + caret.tag = new_line.tags; + caret.pos = 0; + + if (selection_visible == false) { + SetSelectionToCaret (true); + } + } + + if (move_sel_start) { + selection_start.line = new_line; + selection_start.pos = 0; + selection_start.tag = new_line.tags; + } + + if (move_sel_end) { + selection_end.line = new_line; + selection_end.pos = 0; + selection_end.tag = new_line.tags; + } + +#if DEBUG + SanityCheck (); +#endif + return; + } + + // We need to move the rest of the text into the new line + Add (line.line_no + 1, line.text.ToString (pos, line.text.Length - pos), line.alignment, tag.Font, tag.Color, line.ending); + + // Now transfer our tags from this line to the next + new_line = GetLine(line.line_no + 1); + + line.recalc = true; + new_line.recalc = true; + + //make sure that if we are at the end of a tag, we start on the begining + //of a new one, if one exists... Stops us creating an empty tag and + //make the operation easier. + if (tag.Next != null && (tag.Next.Start - 1) == pos) + tag = tag.Next; + + if ((tag.Start - 1) == pos) { + int shift; + + // We can simply break the chain and move the tag into the next line + + // if the tag we are moving is the first, create an empty tag + // for the line we are leaving behind + if (tag == line.tags) { + new_tag = new LineTag(line, 1); + new_tag.CopyFormattingFrom (tag); + line.tags = new_tag; + } + + if (tag.Previous != null) { + tag.Previous.Next = null; + } + new_line.tags = tag; + tag.Previous = null; + tag.Line = new_line; + + // Walk the list and correct the start location of the tags we just bumped into the next line + shift = tag.Start - 1; + + new_tag = tag; + while (new_tag != null) { + new_tag.Start -= shift; + new_tag.Line = new_line; + new_tag = new_tag.Next; + } + } else { + int shift; + + new_tag = new LineTag (new_line, 1); + new_tag.Next = tag.Next; + new_tag.CopyFormattingFrom (tag); + new_line.tags = new_tag; + if (new_tag.Next != null) { + new_tag.Next.Previous = new_tag; + } + tag.Next = null; + + shift = pos; + new_tag = new_tag.Next; + while (new_tag != null) { + new_tag.Start -= shift; + new_tag.Line = new_line; + new_tag = new_tag.Next; + + } + } + + if (move_caret) { + caret.line = new_line; + caret.pos = caret.pos - pos; + caret.tag = caret.line.FindTag(caret.pos); + + if (selection_visible == false) { + SetSelectionToCaret (true); + move_sel_start = false; + move_sel_end = false; + } + } + + if (move_sel_start) { + selection_start.line = new_line; + selection_start.pos = selection_start.pos - pos; + if (selection_start.Equals(selection_end)) + selection_start.tag = new_line.FindTag(selection_start.pos); + else + selection_start.tag = new_line.FindTag (selection_start.pos + 1); + } + + if (move_sel_end) { + selection_end.line = new_line; + selection_end.pos = selection_end.pos - pos; + selection_end.tag = new_line.FindTag(selection_end.pos); + } + + CharCount -= line.text.Length - pos; + line.text.Remove(pos, line.text.Length - pos); +#if DEBUG + SanityCheck (); +#endif + } + +#if DEBUG + private void SanityCheck () { + for (int i = 1; i < lines; i++) { + LineTag tag = GetLine (i).tags; + + if (tag.Start != 1) + throw new Exception ("Line doesn't start at the begining"); + + int start = 1; + tag = tag.Next; + + while (tag != null) { + if (tag.Start == start) + throw new Exception ("Empty tag!"); + + if (tag.Start < start) + throw new Exception ("Insane!!"); + + start = tag.Start; + tag = tag.Next; + } + } + } +#endif + + // Adds a line of text, with given font. + // Bumps any line at that line number that already exists down + internal void Add (int LineNo, string Text, Font font, Color color, LineEnding ending) + { + Add (LineNo, Text, alignment, font, color, ending); + } + + internal void Add (int LineNo, string Text, HorizontalAlignment align, Font font, Color color, LineEnding ending) + { + Line add; + Line line; + int line_no; + + CharCount += Text.Length; + + if (LineNo<1 || Text == null) { + if (LineNo<1) { + throw new ArgumentNullException("LineNo", "Line numbers must be positive"); + } else { + throw new ArgumentNullException("Text", "Cannot insert NULL line"); + } + } + + add = new Line (this, LineNo, Text, align, font, color, ending); + + line = document; + while (line != sentinel) { + add.parent = line; + line_no = line.line_no; + + if (LineNo > line_no) { + line = line.right; + } else if (LineNo < line_no) { + line = line.left; + } else { + // Bump existing line numbers; walk all nodes to the right of this one and increment line_no + IncrementLines(line.line_no); + line = line.left; + } + } + + add.left = sentinel; + add.right = sentinel; + + if (add.parent != null) { + if (LineNo > add.parent.line_no) { + add.parent.right = add; + } else { + add.parent.left = add; + } + } else { + // Root node + document = add; + } + + RebalanceAfterAdd(add); + + lines++; + } + + internal virtual void Clear() { + lines = 0; + CharCount = 0; + document = sentinel; + } + + public virtual object Clone() { + Document clone; + + clone = new Document(null); + + clone.lines = this.lines; + clone.document = (Line)document.Clone(); + + return clone; + } + + private void Delete (int LineNo) + { + Line line; + + if (LineNo > lines) + return; + + line = GetLine (LineNo); + + CharCount -= line.text.Length; + + DecrementLines (LineNo + 1); + Delete (line); + } + + private void Delete(Line line1) { + Line line2;// = new Line(); + Line line3; + + if ((line1.left == sentinel) || (line1.right == sentinel)) { + line3 = line1; + } else { + line3 = line1.right; + while (line3.left != sentinel) { + line3 = line3.left; + } + } + + if (line3.left != sentinel) { + line2 = line3.left; + } else { + line2 = line3.right; + } + + line2.parent = line3.parent; + if (line3.parent != null) { + if(line3 == line3.parent.left) { + line3.parent.left = line2; + } else { + line3.parent.right = line2; + } + } else { + document = line2; + } + + if (line3 != line1) { + LineTag tag; + + if (selection_start.line == line3) { + selection_start.line = line1; + } + + if (selection_end.line == line3) { + selection_end.line = line1; + } + + if (selection_anchor.line == line3) { + selection_anchor.line = line1; + } + + if (caret.line == line3) { + caret.line = line1; + } + + + line1.alignment = line3.alignment; + line1.ascent = line3.ascent; + line1.hanging_indent = line3.hanging_indent; + line1.height = line3.height; + line1.indent = line3.indent; + line1.line_no = line3.line_no; + line1.recalc = line3.recalc; + line1.right_indent = line3.right_indent; + line1.ending = line3.ending; + line1.space = line3.space; + line1.tags = line3.tags; + line1.text = line3.text; + line1.widths = line3.widths; + line1.offset = line3.offset; + + tag = line1.tags; + while (tag != null) { + tag.Line = line1; + tag = tag.Next; + } + } + + if (line3.color == LineColor.Black) + RebalanceAfterDelete(line2); + + this.lines--; + } + + // Invalidates the start line until the end of the viewstate + internal void InvalidateLinesAfter (Line start) { + owner.Invalidate (new Rectangle (0, start.Y - viewport_y, viewport_width, viewport_height - start.Y)); + } + + // Invalidate a section of the document to trigger redraw + internal void Invalidate(Line start, int start_pos, Line end, int end_pos) { + Line l1; + Line l2; + int p1; + int p2; + + if ((start == end) && (start_pos == end_pos)) { + return; + } + + if (end_pos == -1) { + end_pos = end.text.Length; + } + + // figure out what's before what so the logic below is straightforward + if (start.line_no < end.line_no) { + l1 = start; + p1 = start_pos; + + l2 = end; + p2 = end_pos; + } else if (start.line_no > end.line_no) { + l1 = end; + p1 = end_pos; + + l2 = start; + p2 = start_pos; + } else { + if (start_pos < end_pos) { + l1 = start; + p1 = start_pos; + + l2 = end; + p2 = end_pos; + } else { + l1 = end; + p1 = end_pos; + + l2 = start; + p2 = start_pos; + } + + int endpoint = (int) l1.widths [p2]; + if (p2 == l1.text.Length + 1) { + endpoint = (int) viewport_width; + } + + #if Debug + Console.WriteLine("Invaliding backwards from {0}:{1} to {2}:{3} {4}", + l1.line_no, p1, l2.line_no, p2, + new Rectangle( + (int)l1.widths[p1] + l1.X - viewport_x, + l1.Y - viewport_y, + (int)l1.widths[p2], + l1.height + ) + ); + #endif + + owner.Invalidate(new Rectangle ( + offset_x + (int)l1.widths[p1] + l1.X - viewport_x, + offset_y + l1.Y - viewport_y, + endpoint - (int) l1.widths [p1] + 1, + l1.height)); + return; + } + + #if Debug + 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.X - viewport_x, l1.Y - viewport_y, viewport_width, l1.height); + Console.WriteLine ("invalidate start line: {0} position: {1}", l1.text, p1); + #endif + + // Three invalidates: + // First line from start + owner.Invalidate(new Rectangle( + offset_x + (int)l1.widths[p1] + l1.X - viewport_x, + offset_y + l1.Y - viewport_y, + viewport_width, + l1.height)); + + + // lines inbetween + if ((l1.line_no + 1) < l2.line_no) { + int y; + + y = GetLine(l1.line_no + 1).Y; + owner.Invalidate(new Rectangle( + offset_x, + offset_y + y - viewport_y, + viewport_width, + l2.Y - y)); + + #if Debug + Console.WriteLine("Invaliding from {0}:{1} to {2}:{3} Middle => x={4}, y={5}, {6}x{7}", l1.line_no, p1, l2.line_no, p2, 0, y - viewport_y, viewport_width, l2.Y - y); + #endif + } + + + // Last line to end + owner.Invalidate(new Rectangle( + offset_x + (int)l2.widths[0] + l2.X - viewport_x, + offset_y + l2.Y - viewport_y, + (int)l2.widths[p2] + 1, + l2.height)); + + #if Debug + 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.X - viewport_x, l2.Y - viewport_y, (int)l2.widths[p2] + 1, l2.height); + #endif + } + + /// <summary>Select text around caret</summary> + internal void ExpandSelection(CaretSelection mode, bool to_caret) { + if (to_caret) { + // We're expanding the selection to the caret position + switch(mode) { + case CaretSelection.Line: { + // Invalidate the selection delta + if (caret > selection_prev) { + Invalidate(selection_prev.line, 0, caret.line, caret.line.text.Length); + } else { + Invalidate(selection_prev.line, selection_prev.line.text.Length, caret.line, 0); + } + + if (caret.line.line_no <= selection_anchor.line.line_no) { + selection_start.line = caret.line; + selection_start.tag = caret.line.tags; + selection_start.pos = 0; + + selection_end.line = selection_anchor.line; + selection_end.tag = selection_anchor.tag; + selection_end.pos = selection_anchor.pos; + + selection_end_anchor = true; + } else { + selection_start.line = selection_anchor.line; + selection_start.pos = selection_anchor.height; + selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height + 1); + + selection_end.line = caret.line; + selection_end.tag = caret.line.tags; + selection_end.pos = caret.line.text.Length; + + selection_end_anchor = false; + } + selection_prev.line = caret.line; + selection_prev.tag = caret.tag; + selection_prev.pos = caret.pos; + + break; + } + + case CaretSelection.Word: { + int start_pos; + int end_pos; + + start_pos = FindWordSeparator(caret.line, caret.pos, false); + end_pos = FindWordSeparator(caret.line, caret.pos, true); + + + // Invalidate the selection delta + if (caret > selection_prev) { + Invalidate(selection_prev.line, selection_prev.pos, caret.line, end_pos); + } else { + Invalidate(selection_prev.line, selection_prev.pos, caret.line, start_pos); + } + if (caret < selection_anchor) { + selection_start.line = caret.line; + selection_start.tag = caret.line.FindTag(start_pos + 1); + selection_start.pos = start_pos; + + selection_end.line = selection_anchor.line; + selection_end.tag = selection_anchor.tag; + selection_end.pos = selection_anchor.pos; + + selection_prev.line = caret.line; + selection_prev.tag = caret.tag; + selection_prev.pos = start_pos; + + selection_end_anchor = true; + } else { + selection_start.line = selection_anchor.line; + selection_start.pos = selection_anchor.height; + selection_start.tag = selection_anchor.line.FindTag(selection_anchor.height + 1); + + selection_end.line = caret.line; + selection_end.tag = caret.line.FindTag(end_pos); + selection_end.pos = end_pos; + + selection_prev.line = caret.line; + selection_prev.tag = caret.tag; + selection_prev.pos = end_pos; + + selection_end_anchor = false; + } + break; + } + + case CaretSelection.Position: { + SetSelectionToCaret(false); + return; + } + } + } else { + // We're setting the selection 'around' the caret position + switch(mode) { + case CaretSelection.Line: { + this.Invalidate(caret.line, 0, caret.line, caret.line.text.Length); + + selection_start.line = caret.line; + selection_start.tag = caret.line.tags; + selection_start.pos = 0; + + selection_end.line = caret.line; + selection_end.pos = caret.line.text.Length; + selection_end.tag = caret.line.FindTag(selection_end.pos); + + selection_anchor.line = selection_end.line; + selection_anchor.tag = selection_end.tag; + selection_anchor.pos = selection_end.pos; + selection_anchor.height = 0; + + selection_prev.line = caret.line; + selection_prev.tag = caret.tag; + selection_prev.pos = caret.pos; + + this.selection_end_anchor = true; + + break; + } + + case CaretSelection.Word: { + int start_pos; + int end_pos; + + start_pos = FindWordSeparator(caret.line, caret.pos, false); + end_pos = FindWordSeparator(caret.line, caret.pos, true); + + this.Invalidate(selection_start.line, start_pos, caret.line, end_pos); + + selection_start.line = caret.line; + selection_start.tag = caret.line.FindTag(start_pos + 1); + selection_start.pos = start_pos; + + selection_end.line = caret.line; + selection_end.tag = caret.line.FindTag(end_pos); + selection_end.pos = end_pos; + + selection_anchor.line = selection_end.line; + selection_anchor.tag = selection_end.tag; + selection_anchor.pos = selection_end.pos; + selection_anchor.height = start_pos; + + selection_prev.line = caret.line; + selection_prev.tag = caret.tag; + selection_prev.pos = caret.pos; + + this.selection_end_anchor = true; + + break; + } + } + } + + SetSelectionVisible (!(selection_start == selection_end)); + } + + internal void SetSelectionToCaret(bool start) { + if (start) { + // Invalidate old selection; selection is being reset to empty + this.Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + + selection_start.line = caret.line; + selection_start.tag = caret.tag; + selection_start.pos = caret.pos; + + // start always also selects end + selection_end.line = caret.line; + selection_end.tag = caret.tag; + selection_end.pos = caret.pos; + + selection_anchor.line = caret.line; + selection_anchor.tag = caret.tag; + selection_anchor.pos = caret.pos; + } else { + // Invalidate from previous end to caret (aka new end) + if (selection_end_anchor) { + if (selection_start != caret) { + this.Invalidate(selection_start.line, selection_start.pos, caret.line, caret.pos); + } + } else { + if (selection_end != caret) { + this.Invalidate(selection_end.line, selection_end.pos, caret.line, caret.pos); + } + } + + if (caret < selection_anchor) { + selection_start.line = caret.line; + selection_start.tag = caret.tag; + selection_start.pos = caret.pos; + + selection_end.line = selection_anchor.line; + selection_end.tag = selection_anchor.tag; + selection_end.pos = selection_anchor.pos; + + selection_end_anchor = true; + } else { + selection_start.line = selection_anchor.line; + selection_start.tag = selection_anchor.tag; + selection_start.pos = selection_anchor.pos; + + selection_end.line = caret.line; + selection_end.tag = caret.tag; + selection_end.pos = caret.pos; + + selection_end_anchor = false; + } + } + + SetSelectionVisible (!(selection_start == selection_end)); + } + + internal void SetSelection(Line start, int start_pos, Line end, int end_pos) { + if (selection_visible) { + Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + } + + if ((end.line_no < start.line_no) || ((end == start) && (end_pos <= start_pos))) { + selection_start.line = end; + selection_start.tag = LineTag.FindTag(end, end_pos); + selection_start.pos = end_pos; + + selection_end.line = start; + selection_end.tag = LineTag.FindTag(start, start_pos); + selection_end.pos = start_pos; + + selection_end_anchor = true; + } else { + selection_start.line = start; + selection_start.tag = LineTag.FindTag(start, start_pos); + selection_start.pos = start_pos; + + selection_end.line = end; + selection_end.tag = LineTag.FindTag(end, end_pos); + selection_end.pos = end_pos; + + selection_end_anchor = false; + } + + selection_anchor.line = start; + selection_anchor.tag = selection_start.tag; + selection_anchor.pos = start_pos; + + if (((start == end) && (start_pos == end_pos)) || start == null || end == null) { + SetSelectionVisible (false); + } else { + SetSelectionVisible (true); + Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + } + } + + internal void SetSelectionStart(Line start, int start_pos, bool invalidate) { + // Invalidate from the previous to the new start pos + if (invalidate) + Invalidate(selection_start.line, selection_start.pos, start, start_pos); + + selection_start.line = start; + selection_start.pos = start_pos; + selection_start.tag = LineTag.FindTag(start, start_pos); + + selection_anchor.line = start; + selection_anchor.pos = start_pos; + selection_anchor.tag = selection_start.tag; + + selection_end_anchor = false; + + + if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) { + SetSelectionVisible (true); + } else { + SetSelectionVisible (false); + } + + if (invalidate) + Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + } + + internal void SetSelectionStart(int character_index, bool invalidate) { + Line line; + LineTag tag; + int pos; + + if (character_index < 0) { + return; + } + + CharIndexToLineTag(character_index, out line, out tag, out pos); + SetSelectionStart(line, pos, invalidate); + } + + internal void SetSelectionEnd(Line end, int end_pos, bool invalidate) { + + if (end == selection_end.line && end_pos == selection_start.pos) { + selection_anchor.line = selection_start.line; + selection_anchor.tag = selection_start.tag; + selection_anchor.pos = selection_start.pos; + + selection_end.line = selection_start.line; + selection_end.tag = selection_start.tag; + selection_end.pos = selection_start.pos; + + selection_end_anchor = false; + } else if ((end.line_no < selection_anchor.line.line_no) || ((end == selection_anchor.line) && (end_pos <= selection_anchor.pos))) { + selection_start.line = end; + selection_start.tag = LineTag.FindTag(end, end_pos); + selection_start.pos = end_pos; + + selection_end.line = selection_anchor.line; + selection_end.tag = selection_anchor.tag; + selection_end.pos = selection_anchor.pos; + + selection_end_anchor = true; + } else { + selection_start.line = selection_anchor.line; + selection_start.tag = selection_anchor.tag; + selection_start.pos = selection_anchor.pos; + + selection_end.line = end; + selection_end.tag = LineTag.FindTag(end, end_pos); + selection_end.pos = end_pos; + + selection_end_anchor = false; + } + + if ((selection_end.line != selection_start.line) || (selection_end.pos != selection_start.pos)) { + SetSelectionVisible (true); + if (invalidate) + Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + } else { + SetSelectionVisible (false); + // ?? Do I need to invalidate here, tests seem to work without it, but I don't think they should :-s + } + } + + internal void SetSelectionEnd(int character_index, bool invalidate) { + Line line; + LineTag tag; + int pos; + + if (character_index < 0) { + return; + } + + CharIndexToLineTag(character_index, out line, out tag, out pos); + SetSelectionEnd(line, pos, invalidate); + } + + internal void SetSelection(Line start, int start_pos) { + if (selection_visible) { + Invalidate(selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + } + + selection_start.line = start; + selection_start.pos = start_pos; + selection_start.tag = LineTag.FindTag(start, start_pos); + + selection_end.line = start; + selection_end.tag = selection_start.tag; + selection_end.pos = start_pos; + + selection_anchor.line = start; + selection_anchor.tag = selection_start.tag; + selection_anchor.pos = start_pos; + + selection_end_anchor = false; + SetSelectionVisible (false); + } + + internal void InvalidateSelectionArea() { + Invalidate (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + } + + // Return the current selection, as string + internal string GetSelection() { + // We return String.Empty if there is no selection + if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) { + return string.Empty; + } + + if (selection_start.line == selection_end.line) { + return selection_start.line.text.ToString (selection_start.pos, selection_end.pos - selection_start.pos); + } else { + StringBuilder sb; + int i; + int start; + int end; + + sb = new StringBuilder(); + start = selection_start.line.line_no; + end = selection_end.line.line_no; + + sb.Append(selection_start.line.text.ToString(selection_start.pos, selection_start.line.text.Length - selection_start.pos)); + + if ((start + 1) < end) { + for (i = start + 1; i < end; i++) { + sb.Append(GetLine(i).text.ToString()); + } + } + + sb.Append(selection_end.line.text.ToString(0, selection_end.pos)); + + return sb.ToString(); + } + } + + internal void ReplaceSelection(string s, bool select_new) { + int i; + + int selection_start_pos = LineTagToCharIndex (selection_start.line, selection_start.pos); + SuspendRecalc (); + + // First, delete any selected text + if ((selection_start.pos != selection_end.pos) || (selection_start.line != selection_end.line)) { + if (selection_start.line == selection_end.line) { + undo.RecordDeleteString (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + + DeleteChars (selection_start.line, selection_start.pos, selection_end.pos - selection_start.pos); + + // The tag might have been removed, we need to recalc it + selection_start.tag = selection_start.line.FindTag(selection_start.pos + 1); + } else { + int start; + int end; + + start = selection_start.line.line_no; + end = selection_end.line.line_no; + + undo.RecordDeleteString (selection_start.line, selection_start.pos, selection_end.line, selection_end.pos); + + InvalidateLinesAfter(selection_start.line); + + // Delete first line + DeleteChars (selection_start.line, selection_start.pos, selection_start.line.text.Length - selection_start.pos); + selection_start.line.recalc = true; + + // Delete last line + DeleteChars(selection_end.line, 0, selection_end.pos); + + start++; + if (start < end) { + for (i = end - 1; i >= start; i--) { + Delete(i); + } + } + + // BIG FAT WARNING - selection_end.line might be stale due + // to the above Delete() call. DONT USE IT before hitting the end of this method! + + // Join start and end + Combine(selection_start.line.line_no, start); + } + } + + + Insert(selection_start.line, selection_start.pos, false, s); + undo.RecordInsertString (selection_start.line, selection_start.pos, s); + ResumeRecalc (false); + + Line begin_update_line = selection_start.line; + int begin_update_pos = selection_start.pos; + + if (!select_new) { + CharIndexToLineTag(selection_start_pos + s.Length, out selection_start.line, + out selection_start.tag, out selection_start.pos); + + selection_end.line = selection_start.line; + selection_end.pos = selection_start.pos; + selection_end.tag = selection_start.tag; + selection_anchor.line = selection_start.line; + selection_anchor.pos = selection_start.pos; + selection_anchor.tag = selection_start.tag; + + SetSelectionVisible (false); + } else { + CharIndexToLineTag(selection_start_pos, out selection_start.line, + out selection_start.tag, out selection_start.pos); + + CharIndexToLineTag(selection_start_pos + s.Length, out selection_end.line, + out selection_end.tag, out selection_end.pos); + + selection_anchor.line = selection_start.line; + selection_anchor.pos = selection_start.pos; + selection_anchor.tag = selection_start.tag; + + SetSelectionVisible (true); + } + + PositionCaret (selection_start.line, selection_start.pos); + UpdateView (begin_update_line, selection_end.line.line_no - begin_update_line.line_no, begin_update_pos); + } + + internal void CharIndexToLineTag(int index, out Line line_out, out LineTag tag_out, out int pos) { + Line line; + LineTag tag; + int i; + int chars; + int start; + + chars = 0; + + for (i = 1; i <= lines; i++) { + line = GetLine(i); + + start = chars; + chars += line.text.Length; + + if (index <= chars) { + // we found the line + tag = line.tags; + + while (tag != null) { + if (index < (start + tag.Start + tag.Length - 1)) { + line_out = line; + tag_out = LineTag.GetFinalTag (tag); + pos = index - start; + return; + } + if (tag.Next == null) { + Line next_line; + + next_line = GetLine(line.line_no + 1); + + if (next_line != null) { + line_out = next_line; + tag_out = LineTag.GetFinalTag (next_line.tags); + pos = 0; + return; + } else { + line_out = line; + tag_out = LineTag.GetFinalTag (tag); + pos = line_out.text.Length; + return; + } + } + tag = tag.Next; + } + } + } + + line_out = GetLine(lines); + tag = line_out.tags; + while (tag.Next != null) { + tag = tag.Next; + } + tag_out = tag; + pos = line_out.text.Length; + } + + internal int LineTagToCharIndex(Line line, int pos) { + int i; + int length; + + // Count first and last line + length = 0; + + // Count the lines in the middle + + for (i = 1; i < line.line_no; i++) { + length += GetLine(i).text.Length; + } + + length += pos; + + return length; + } + + internal int SelectionLength() { + if ((selection_start.pos == selection_end.pos) && (selection_start.line == selection_end.line)) { + return 0; + } + + if (selection_start.line == selection_end.line) { + return selection_end.pos - selection_start.pos; + } else { + int i; + int start; + int end; + int length; + + // Count first and last line + length = selection_start.line.text.Length - selection_start.pos + selection_end.pos + crlf_size; + + // Count the lines in the middle + start = selection_start.line.line_no + 1; + end = selection_end.line.line_no; + + if (start < end) { + for (i = start; i < end; i++) { + Line line = GetLine (i); + length += line.text.Length + LineEndingLength (line.ending); + } + } + + return length; + } + + + } + + + // UIA: Method used via reflection in TextRangeProvider + + /// <summary>Give it a Line number and it returns the Line object at with that line number</summary> + internal Line GetLine(int LineNo) { + Line line = document; + + while (line != sentinel) { + if (LineNo == line.line_no) { + return line; + } else if (LineNo < line.line_no) { + line = line.left; + } else { + line = line.right; + } + } + + return null; + } + + /// <summary>Retrieve the previous tag; walks line boundaries</summary> + internal LineTag PreviousTag(LineTag tag) { + Line l; + + if (tag.Previous != null) { + return tag.Previous; + } + + // Next line + if (tag.Line.line_no == 1) { + return null; + } + + l = GetLine(tag.Line.line_no - 1); + if (l != null) { + LineTag t; + + t = l.tags; + while (t.Next != null) { + t = t.Next; + } + return t; + } + + return null; + } + + /// <summary>Retrieve the next tag; walks line boundaries</summary> + internal LineTag NextTag(LineTag tag) { + Line l; + + if (tag.Next != null) { + return tag.Next; + } + + // Next line + l = GetLine(tag.Line.line_no + 1); + if (l != null) { + return l.tags; + } + + return null; + } + + internal Line ParagraphStart(Line line) { + Line lastline = line; + do { + if (line.line_no <= 1) + break; + + line = lastline; + lastline = GetLine (line.line_no - 1); + } while (lastline.ending == LineEnding.Wrap); + + return line; + } + + internal Line ParagraphEnd(Line line) { + Line l; + + while (line.ending == LineEnding.Wrap) { + l = GetLine(line.line_no + 1); + if ((l == null) || (l.ending != LineEnding.Wrap)) { + break; + } + line = l; + } + return line; + } + + /// <summary>Give it a pixel offset coordinate and it returns the Line covering that are (offset + /// is either X or Y depending on if we are multiline + /// </summary> + internal Line GetLineByPixel (int offset, bool exact) + { + Line line = document; + Line last = null; + + if (multiline) { + while (line != sentinel) { + last = line; + if ((offset >= line.Y) && (offset < (line.Y+line.height))) { + return line; + } else if (offset < line.Y) { + line = line.left; + } else { + line = line.right; + } + } + } else { + while (line != sentinel) { + last = line; + if ((offset >= line.X) && (offset < (line.X + line.Width))) + return line; + else if (offset < line.X) + line = line.left; + else + line = line.right; + } + } + + if (exact) { + return null; + } + return last; + } + + // UIA: Method used via reflection in TextProviderBehavior + + // Give it x/y pixel coordinates and it returns the Tag at that position + internal LineTag FindCursor (int x, int y, out int index) + { + Line line; + + x -= offset_x; + y -= offset_y; + + line = GetLineByPixel (multiline ? y : x, false); + + LineTag tag = line.GetTag (x); + + if (tag.Length == 0 && tag.Start == 1) + index = 0; + else + index = tag.GetCharIndex (x - line.align_shift); + + return tag; + } + + /// <summary>Format area of document in specified font and color</summary> + /// <param name="start_pos">1-based start position on start_line</param> + /// <param name="end_pos">1-based end position on end_line </param> + internal void FormatText (Line start_line, int start_pos, Line end_line, int end_pos, Font font, + Color color, Color back_color, FormatSpecified specified) + { + Line l; + + // First, format the first line + if (start_line != end_line) { + // First line + LineTag.FormatText(start_line, start_pos, start_line.text.Length - start_pos + 1, font, color, back_color, specified); + + // Format last line + LineTag.FormatText(end_line, 1, end_pos, font, color, back_color, specified); + + // Now all the lines inbetween + for (int i = start_line.line_no + 1; i < end_line.line_no; i++) { + l = GetLine(i); + LineTag.FormatText(l, 1, l.text.Length, font, color, back_color, specified); + } + } else { + // Special case, single line + LineTag.FormatText(start_line, start_pos, end_pos - start_pos, font, color, back_color, specified); + + if ((end_pos - start_pos) == 0 && CaretTag.Length != 0) + CaretTag = CaretTag.Next; + } + } + + internal void RecalculateAlignments () + { + Line line; + int line_no; + + line_no = 1; + + + + while (line_no <= lines) { + line = GetLine(line_no); + + if (line != null) { + switch (line.alignment) { + case HorizontalAlignment.Left: + line.align_shift = 0; + break; + case HorizontalAlignment.Center: + line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2; + break; + case HorizontalAlignment.Right: + line.align_shift = viewport_width - (int)line.widths[line.text.Length] - right_margin; + break; + } + } + + line_no++; + } + return; + } + + /// <summary>Calculate formatting for the whole document</summary> + internal bool RecalculateDocument(Graphics g) { + return RecalculateDocument(g, 1, this.lines, false); + } + + /// <summary>Calculate formatting starting at a certain line</summary> + internal bool RecalculateDocument(Graphics g, int start) { + return RecalculateDocument(g, start, this.lines, false); + } + + /// <summary>Calculate formatting within two given line numbers</summary> + internal bool RecalculateDocument(Graphics g, int start, int end) { + return RecalculateDocument(g, start, end, false); + } + + /// <summary>With optimize on, returns true if line heights changed</summary> + internal bool RecalculateDocument(Graphics g, int start, int end, bool optimize) { + Line line; + int line_no; + int offset; + int new_width; + bool changed; + int shift; + + if (recalc_suspended > 0) { + recalc_pending = true; + recalc_start = Math.Min (recalc_start, start); + recalc_end = Math.Max (recalc_end, end); + recalc_optimize = optimize; + return false; + } + + // Fixup the positions, they can go kinda nuts + // (this is suspend and resume recalc - they set them to 1 and max) + start = Math.Max (start, 1); + end = Math.Min (end, lines); + + offset = GetLine(start).offset; + line_no = start; + new_width = 0; + shift = this.lines; + if (!optimize) { + changed = true; // We always return true if we run non-optimized + } else { + changed = false; + } + + while (line_no <= (end + this.lines - shift)) { + line = GetLine(line_no++); + line.offset = offset; + + // if we are not calculating a password + if (!calc_pass) { + if (!optimize) { + line.RecalculateLine(g, this); + } else { + if (line.recalc && line.RecalculateLine(g, this)) { + changed = true; + // If the height changed, all subsequent lines change + end = this.lines; + shift = this.lines; + } + } + } else { + if (!optimize) { + line.RecalculatePasswordLine(g, this); + } else { + if (line.recalc && line.RecalculatePasswordLine(g, this)) { + changed = true; + // If the height changed, all subsequent lines change + end = this.lines; + shift = this.lines; + } + } + } + + if (line.widths[line.text.Length] > new_width) { + new_width = (int)line.widths[line.text.Length]; + } + + // Calculate alignment + if (line.alignment != HorizontalAlignment.Left) { + if (line.alignment == HorizontalAlignment.Center) { + line.align_shift = (viewport_width - (int)line.widths[line.text.Length]) / 2; + } else { + line.align_shift = viewport_width - (int)line.widths[line.text.Length] - 1; + } + } + + if (multiline) + offset += line.height; + else + offset += (int) line.widths [line.text.Length]; + + if (line_no > lines) { + break; + } + } + + if (document_x != new_width) { + document_x = new_width; + if (WidthChanged != null) { + WidthChanged(this, null); + } + } + + RecalculateAlignments(); + + line = GetLine(lines); + + if (document_y != line.Y + line.height) { + document_y = line.Y + line.height; + if (HeightChanged != null) { + HeightChanged(this, null); + } + } + + // scan for links and tell us if its all + // changed, so we can update everything + if (EnableLinks) + ScanForLinks (start, end, ref changed); + + UpdateCaret(); + return changed; + } + + internal int Size() { + return lines; + } + + private void owner_HandleCreated(object sender, EventArgs e) { + RecalculateDocument(owner.CreateGraphicsInternal()); + AlignCaret(); + } + + private void owner_VisibleChanged(object sender, EventArgs e) { + if (owner.Visible) { + RecalculateDocument(owner.CreateGraphicsInternal()); + } + } + + internal static bool IsWordSeparator (char ch) + { + switch (ch) { + case ' ': + case '\t': + case '(': + case ')': + case '\r': + case '\n': + return true; + default: + return false; + } + } + + internal int FindWordSeparator(Line line, int pos, bool forward) { + int len; + + len = line.text.Length; + + if (forward) { + for (int i = pos + 1; i < len; i++) { + if (IsWordSeparator(line.Text[i])) { + return i + 1; + } + } + return len; + } else { + for (int i = pos - 1; i > 0; i--) { + if (IsWordSeparator(line.Text[i - 1])) { + return i; + } + } + return 0; + } + } + + /* Search document for text */ + internal bool FindChars(char[] chars, Marker start, Marker end, out Marker result) { + Line line; + int line_no; + int pos; + int line_len; + + // Search for occurence of any char in the chars array + result = new Marker(); + + line = start.line; + line_no = start.line.line_no; + pos = start.pos; + while (line_no <= end.line.line_no) { + line_len = line.text.Length; + while (pos < line_len) { + for (int i = 0; i < chars.Length; i++) { + if (line.text[pos] == chars[i]) { + // Special case + if ((line.line_no == end.line.line_no) && (pos >= end.pos)) { + return false; + } + + result.line = line; + result.pos = pos; + return true; + } + } + pos++; + } + + pos = 0; + line_no++; + line = GetLine(line_no); + } + + return false; + } + + // This version does not build one big string for searching, instead it handles + // line-boundaries, which is faster and less memory intensive + // FIXME - Depending on culture stuff we might have to create a big string and use culturespecific + // search stuff and change it to accept and return positions instead of Markers (which would match + // RichTextBox behaviour better but would be inconsistent with the rest of TextControl) + internal bool Find(string search, Marker start, Marker end, out Marker result, RichTextBoxFinds options) { + Marker last; + string search_string; + Line line; + int line_no; + int pos; + int line_len; + int current; + bool word; + bool word_option; + bool ignore_case; + bool reverse; + char c; + + result = new Marker(); + word_option = ((options & RichTextBoxFinds.WholeWord) != 0); + ignore_case = ((options & RichTextBoxFinds.MatchCase) == 0); + reverse = ((options & RichTextBoxFinds.Reverse) != 0); + + line = start.line; + line_no = start.line.line_no; + pos = start.pos; + current = 0; + + // Prep our search string, lowercasing it if we do case-independent matching + if (ignore_case) { + StringBuilder sb; + sb = new StringBuilder(search); + for (int i = 0; i < sb.Length; i++) { + sb[i] = Char.ToLower(sb[i]); + } + search_string = sb.ToString(); + } else { + search_string = search; + } + + // We need to check if the character before our start position is a wordbreak + if (word_option) { + if (line_no == 1) { + if ((pos == 0) || (IsWordSeparator(line.text[pos - 1]))) { + word = true; + } else { + word = false; + } + } else { + if (pos > 0) { + if (IsWordSeparator(line.text[pos - 1])) { + word = true; + } else { + word = false; + } + } else { + // Need to check the end of the previous line + Line prev_line; + + prev_line = GetLine(line_no - 1); + if (prev_line.ending == LineEnding.Wrap) { + if (IsWordSeparator(prev_line.text[prev_line.text.Length - 1])) { + word = true; + } else { + word = false; + } + } else { + word = true; + } + } + } + } else { + word = false; + } + + // To avoid duplication of this loop with reverse logic, we search + // through the document, remembering the last match and when returning + // report that last remembered match + + last = new Marker(); + last.height = -1; // Abused - we use it to track change + + while (line_no <= end.line.line_no) { + if (line_no != end.line.line_no) { + line_len = line.text.Length; + } else { + line_len = end.pos; + } + + while (pos < line_len) { + + if (word_option && (current == search_string.Length)) { + if (IsWordSeparator(line.text[pos])) { + if (!reverse) { + goto FindFound; + } else { + last = result; + current = 0; + } + } else { + current = 0; + } + } + + if (ignore_case) { + c = Char.ToLower(line.text[pos]); + } else { + c = line.text[pos]; + } + + if (c == search_string[current]) { + + if (current == 0) { + result.line = line; + result.pos = pos; + } + if (!word_option || (word_option && (word || (current > 0)))) { + current++; + } + + if (!word_option && (current == search_string.Length)) { + if (!reverse) { + goto FindFound; + } else { + last = result; + current = 0; + } + } + } else { + current = 0; + } + pos++; + + if (!word_option) { + continue; + } + + if (IsWordSeparator(c)) { + word = true; + } else { + word = false; + } + } + + if (word_option) { + // Mark that we just saw a word boundary + if (line.ending != LineEnding.Wrap || line.line_no == lines - 1) { + word = true; + } + + if (current == search_string.Length) { + if (word) { + if (!reverse) { + goto FindFound; + } else { + last = result; + current = 0; + } + } else { + current = 0; + } + } + } + + pos = 0; + line_no++; + line = GetLine(line_no); + } + + if (reverse) { + if (last.height != -1) { + result = last; + return true; + } + } + + return false; + + FindFound: + if (!reverse) { +// if ((line.line_no == end.line.line_no) && (pos >= end.pos)) { +// return false; +// } + return true; + } + + result = last; + return true; + + } + + /* Marker stuff */ + internal void GetMarker(out Marker mark, bool start) { + mark = new Marker(); + + if (start) { + mark.line = GetLine(1); + mark.tag = mark.line.tags; + mark.pos = 0; + } else { + mark.line = GetLine(lines); + mark.tag = mark.line.tags; + while (mark.tag.Next != null) { + mark.tag = mark.tag.Next; + } + mark.pos = mark.line.text.Length; + } + } + #endregion // Internal Methods + + #region Events + internal event EventHandler CaretMoved; + internal event EventHandler WidthChanged; + internal event EventHandler HeightChanged; + internal event EventHandler LengthChanged; + internal event EventHandler UIASelectionChanged; + #endregion // Events + + #region Administrative + public IEnumerator GetEnumerator() { + // FIXME + return null; + } + + public override bool Equals(object obj) { + if (obj == null) { + return false; + } + + if (!(obj is Document)) { + return false; + } + + if (obj == this) { + return true; + } + + if (ToString().Equals(((Document)obj).ToString())) { + return true; + } + + return false; + } + + public override int GetHashCode() { + return document_id; + } + + public override string ToString() { + return "document " + this.document_id; + } + #endregion // Administrative + } + + internal class PictureTag : LineTag { + + internal RTF.Picture picture; + + internal PictureTag (Line line, int start, RTF.Picture picture) : base (line, start) + { + this.picture = picture; + } + + public override bool IsTextTag { + get { return false; } + } + + public override SizeF SizeOfPosition (Graphics dc, int pos) + { + return picture.Size; + } + + internal override int MaxHeight () + { + return (int) (picture.Height + 0.5F); + } + + public override void Draw (Graphics dc, Color color, float xoff, float y, int start, int end) + { + picture.DrawImage (dc, xoff + Line.widths [start], y, false); + } + + public override void Draw (Graphics dc, Color color, float xoff, float y, int start, int end, string text) + { + picture.DrawImage (dc, xoff + + Line.widths [start], y, false); + } + + public override string Text () + { + return "I"; + } + } + + internal class UndoManager { + + internal enum ActionType { + + Typing, + + // This is basically just cut & paste + InsertString, + DeleteString, + + UserActionBegin, + UserActionEnd + } + + internal class Action { + internal ActionType type; + internal int line_no; + internal int pos; + internal object data; + } + + #region Local Variables + private Document document; + private Stack undo_actions; + private Stack redo_actions; + + //private int caret_line; + //private int caret_pos; + + // When performing an action, we lock the queue, so that the action can't be undone + private bool locked; + #endregion // Local Variables + + #region Constructors + internal UndoManager (Document document) + { + this.document = document; + undo_actions = new Stack (50); + redo_actions = new Stack (50); + } + #endregion // Constructors + + #region Properties + internal bool CanUndo { + get { return undo_actions.Count > 0; } + } + + internal bool CanRedo { + get { return redo_actions.Count > 0; } + } + + internal string UndoActionName { + get { + foreach (Action action in undo_actions) { + if (action.type == ActionType.UserActionBegin) + return (string) action.data; + if (action.type == ActionType.Typing) + return String.Format ("Typing"); + } + return String.Empty; + } + } + + internal string RedoActionName { + get { + foreach (Action action in redo_actions) { + if (action.type == ActionType.UserActionBegin) + return (string) action.data; + if (action.type == ActionType.Typing) + return String.Format ("Typing"); + } + return String.Empty; + } + } + #endregion // Properties + + #region Internal Methods + internal void Clear () + { + undo_actions.Clear(); + redo_actions.Clear(); + } + + internal bool Undo () + { + Action action; + bool user_action_finished = false; + + if (undo_actions.Count == 0) + return false; + + locked = true; + do { + Line start; + action = (Action) undo_actions.Pop (); + + // Put onto redo stack + redo_actions.Push(action); + + // Do the thing + switch(action.type) { + + case ActionType.UserActionBegin: + user_action_finished = true; + break; + + case ActionType.UserActionEnd: + // noop + break; + + case ActionType.InsertString: + start = document.GetLine (action.line_no); + document.SuspendUpdate (); + document.DeleteMultiline (start, action.pos, ((string) action.data).Length + 1); + document.PositionCaret (start, action.pos); + document.SetSelectionToCaret (true); + document.ResumeUpdate (true); + break; + + case ActionType.Typing: + start = document.GetLine (action.line_no); + document.SuspendUpdate (); + document.DeleteMultiline (start, action.pos, ((StringBuilder) action.data).Length); + document.PositionCaret (start, action.pos); + document.SetSelectionToCaret (true); + document.ResumeUpdate (true); + + // This is an open ended operation, so only a single typing operation can be undone at once + user_action_finished = true; + break; + + case ActionType.DeleteString: + start = document.GetLine (action.line_no); + document.SuspendUpdate (); + Insert (start, action.pos, (Line) action.data, true); + document.ResumeUpdate (true); + break; + } + } while (!user_action_finished && undo_actions.Count > 0); + + locked = false; + + return true; + } + + internal bool Redo () + { + Action action; + bool user_action_finished = false; + + if (redo_actions.Count == 0) + return false; + + locked = true; + do { + Line start; + int start_index; + + action = (Action) redo_actions.Pop (); + undo_actions.Push (action); + + switch (action.type) { + + case ActionType.UserActionBegin: + // Noop + break; + + case ActionType.UserActionEnd: + user_action_finished = true; + break; + + case ActionType.InsertString: + start = document.GetLine (action.line_no); + document.SuspendUpdate (); + start_index = document.LineTagToCharIndex (start, action.pos); + document.InsertString (start, action.pos, (string) action.data); + document.CharIndexToLineTag (start_index + ((string) action.data).Length, + out document.caret.line, out document.caret.tag, + out document.caret.pos); + document.UpdateCaret (); + document.SetSelectionToCaret (true); + document.ResumeUpdate (true); + break; + + case ActionType.Typing: + start = document.GetLine (action.line_no); + document.SuspendUpdate (); + start_index = document.LineTagToCharIndex (start, action.pos); + document.InsertString (start, action.pos, ((StringBuilder) action.data).ToString ()); + document.CharIndexToLineTag (start_index + ((StringBuilder) action.data).Length, + out document.caret.line, out document.caret.tag, + out document.caret.pos); + document.UpdateCaret (); + document.SetSelectionToCaret (true); + document.ResumeUpdate (true); + + // This is an open ended operation, so only a single typing operation can be undone at once + user_action_finished = true; + break; + + case ActionType.DeleteString: + start = document.GetLine (action.line_no); + document.SuspendUpdate (); + document.DeleteMultiline (start, action.pos, ((Line) action.data).text.Length); + document.PositionCaret (start, action.pos); + document.SetSelectionToCaret (true); + document.ResumeUpdate (true); + + break; + } + } while (!user_action_finished && redo_actions.Count > 0); + + locked = false; + + return true; + } + #endregion // Internal Methods + + #region Private Methods + + public void BeginUserAction (string name) + { + if (locked) + return; + + // Nuke the redo queue + redo_actions.Clear (); + + Action ua = new Action (); + ua.type = ActionType.UserActionBegin; + ua.data = name; + + undo_actions.Push (ua); + } + + public void EndUserAction () + { + if (locked) + return; + + Action ua = new Action (); + ua.type = ActionType.UserActionEnd; + + undo_actions.Push (ua); + } + + // start_pos, end_pos = 1 based + public void RecordDeleteString (Line start_line, int start_pos, Line end_line, int end_pos) + { + if (locked) + return; + + // Nuke the redo queue + redo_actions.Clear (); + + Action a = new Action (); + + // We cant simply store the string, because then formatting would be lost + a.type = ActionType.DeleteString; + a.line_no = start_line.line_no; + a.pos = start_pos; + a.data = Duplicate (start_line, start_pos, end_line, end_pos); + + undo_actions.Push(a); + } + + public void RecordInsertString (Line line, int pos, string str) + { + if (locked || str.Length == 0) + return; + + // Nuke the redo queue + redo_actions.Clear (); + + Action a = new Action (); + + a.type = ActionType.InsertString; + a.data = str; + a.line_no = line.line_no; + a.pos = pos; + + undo_actions.Push (a); + } + + public void RecordTyping (Line line, int pos, char ch) + { + if (locked) + return; + + // Nuke the redo queue + redo_actions.Clear (); + + Action a = null; + + if (undo_actions.Count > 0) + a = (Action) undo_actions.Peek (); + + if (a == null || a.type != ActionType.Typing) { + a = new Action (); + a.type = ActionType.Typing; + a.data = new StringBuilder (); + a.line_no = line.line_no; + a.pos = pos; + + undo_actions.Push (a); + } + + StringBuilder data = (StringBuilder) a.data; + data.Append (ch); + } + + // start_pos = 1-based + // end_pos = 1-based + public Line Duplicate(Line start_line, int start_pos, Line end_line, int end_pos) + { + Line ret; + Line line; + Line current; + LineTag tag; + LineTag current_tag; + int start; + int end; + int tag_start; + + line = new Line (start_line.document, start_line.ending); + ret = line; + + for (int i = start_line.line_no; i <= end_line.line_no; i++) { + current = document.GetLine(i); + + if (start_line.line_no == i) { + start = start_pos; + } else { + start = 0; + } + + if (end_line.line_no == i) { + end = end_pos; + } else { + end = current.text.Length; + } + + if (end_pos == 0) + continue; + + // Text for the tag + line.text = new StringBuilder (current.text.ToString (start, end - start)); + + // Copy tags from start to start+length onto new line + current_tag = current.FindTag (start + 1); + while ((current_tag != null) && (current_tag.Start <= end)) { + if ((current_tag.Start <= start) && (start < (current_tag.Start + current_tag.Length))) { + // start tag is within this tag + tag_start = start; + } else { + tag_start = current_tag.Start; + } + + tag = new LineTag(line, tag_start - start + 1); + tag.CopyFormattingFrom (current_tag); + + current_tag = current_tag.Next; + + // Add the new tag to the line + if (line.tags == null) { + line.tags = tag; + } else { + LineTag tail; + tail = line.tags; + + while (tail.Next != null) { + tail = tail.Next; + } + tail.Next = tag; + tag.Previous = tail; + } + } + + if ((i + 1) <= end_line.line_no) { + line.ending = current.ending; + + // Chain them (we use right/left as next/previous) + line.right = new Line (start_line.document, start_line.ending); + line.right.left = line; + line = line.right; + } + } + + return ret; + } + + // Insert multi-line text at the given position; use formatting at insertion point for inserted text + internal void Insert(Line line, int pos, Line insert, bool select) + { + Line current; + LineTag tag; + int offset; + int lines; + Line first; + + // Handle special case first + if (insert.right == null) { + + // Single line insert + document.Split(line, pos); + + if (insert.tags == null) { + return; // Blank line + } + + //Insert our tags at the end + tag = line.tags; + + while (tag.Next != null) { + tag = tag.Next; + } + + offset = tag.Start + tag.Length - 1; + + tag.Next = insert.tags; + line.text.Insert(offset, insert.text.ToString()); + + // Adjust start locations + tag = tag.Next; + while (tag != null) { + tag.Start += offset; + tag.Line = line; + tag = tag.Next; + } + // Put it back together + document.Combine(line.line_no, line.line_no + 1); + + if (select) { + document.SetSelectionStart (line, pos, false); + document.SetSelectionEnd (line, pos + insert.text.Length, false); + } + + document.UpdateView(line, pos); + return; + } + + first = line; + lines = 1; + current = insert; + + while (current != null) { + + if (current == insert) { + // Inserting the first line we split the line (and make space) + document.Split(line.line_no, pos); + //Insert our tags at the end of the line + tag = line.tags; + + + if (tag != null && tag.Length != 0) { + while (tag.Next != null) { + tag = tag.Next; + } + offset = tag.Start + tag.Length - 1; + tag.Next = current.tags; + tag.Next.Previous = tag; + + tag = tag.Next; + + } else { + offset = 0; + line.tags = current.tags; + line.tags.Previous = null; + tag = line.tags; + } + + line.ending = current.ending; + } else { + document.Split(line.line_no, 0); + offset = 0; + line.tags = current.tags; + line.tags.Previous = null; + line.ending = current.ending; + tag = line.tags; + } + + // Adjust start locations and line pointers + while (tag != null) { + tag.Start += offset - 1; + tag.Line = line; + tag = tag.Next; + } + + line.text.Insert(offset, current.text.ToString()); + line.Grow(line.text.Length); + + line.recalc = true; + line = document.GetLine(line.line_no + 1); + + // FIXME? Test undo of line-boundaries + if ((current.right == null) && (current.tags.Length != 0)) { + document.Combine(line.line_no - 1, line.line_no); + } + current = current.right; + lines++; + + } + + // Recalculate our document + document.UpdateView(first, lines, pos); + return; + } + #endregion // Private Methods + } +} |
