mirror of
https://github.com/seriocomedy/ShiftOS-C-.git
synced 2025-01-23 17:32:15 +00:00
d40fed5ce2
This'll be a lot easier to work on.
4613 lines
116 KiB
C#
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
|
|
}
|
|
}
|