ShiftOS-C-/source/ShiftUI/Internal/TextControl.cs
MichaelTheShifter d40fed5ce2 Move ShiftUI source code to ShiftOS
This'll be a lot easier to work on.
2016-07-20 09:40:36 -04:00

4613 lines
116 KiB
C#

// 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 pbartok@novell.com
//
//
// 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
}
}