1 // Permission is hereby granted, free of charge, to any person obtaining
\r
2 // a copy of this software and associated documentation files (the
\r
3 // "Software"), to deal in the Software without restriction, including
\r
4 // without limitation the rights to use, copy, modify, merge, publish,
\r
5 // distribute, sublicense, and/or sell copies of the Software, and to
\r
6 // permit persons to whom the Software is furnished to do so, subject to
\r
7 // the following conditions:
\r
9 // The above copyright notice and this permission notice shall be
\r
10 // included in all copies or substantial portions of the Software.
\r
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
\r
13 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
\r
14 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
\r
15 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
\r
16 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
\r
17 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
\r
18 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\r
20 // Copyright (c) 2004-2006 Novell, Inc. (http://www.novell.com)
\r
23 // Peter Bartok pbartok@novell.com
\r
28 using System.Collections;
\r
29 using System.Drawing;
\r
30 using System.Drawing.Text;
\r
33 namespace System.Windows.Forms
\r
35 internal class Line : ICloneable, IComparable
\r
37 #region Local Variables
\r
39 internal Document document;
\r
40 // Stuff that matters for our line
\r
41 internal StringBuilder text; // Characters for the line
\r
42 internal float[] widths; // Width of each character; always one larger than text.Length
\r
43 internal int space; // Number of elements in text and widths
\r
44 internal int line_no; // Line number
\r
45 internal LineTag tags; // Tags describing the text
\r
46 internal int offset; // Baseline can be on the X or Y axis depending if we are in multiline mode or not
\r
47 internal int height; // Height of the line (height of tallest tag)
\r
48 internal int ascent; // Ascent of the line (ascent of the tallest tag)
\r
49 internal HorizontalAlignment alignment; // Alignment of the line
\r
50 internal int align_shift; // Pixel shift caused by the alignment
\r
51 internal int indent; // Left indent for the first line
\r
52 internal int hanging_indent; // Hanging indent (left indent for all but the first line)
\r
53 internal int right_indent; // Right indent for all lines
\r
54 internal LineEnding ending;
\r
56 // Stuff that's important for the tree
\r
57 internal Line parent; // Our parent line
\r
58 internal Line left; // Line with smaller line number
\r
59 internal Line right; // Line with higher line number
\r
60 internal LineColor color; // We're doing a black/red tree. this is the node color
\r
61 static int DEFAULT_TEXT_LEN = 0; //
\r
62 internal bool recalc; // Line changed
\r
63 #endregion // Local Variables
\r
65 #region Constructors
\r
66 internal Line (Document document, LineEnding ending)
\r
68 this.document = document;
\r
69 color = LineColor.Red;
\r
75 alignment = document.alignment;
\r
77 this.ending = ending;
\r
80 internal Line (Document document, int LineNo, string Text, Font font, Color color, LineEnding ending) : this (document, ending)
\r
82 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
\r
84 text = new StringBuilder (Text, space);
\r
86 this.ending = ending;
\r
88 widths = new float[space + 1];
\r
91 tags = new LineTag(this, 1);
\r
96 internal Line (Document document, int LineNo, string Text, HorizontalAlignment align, Font font, Color color, LineEnding ending) : this(document, ending)
\r
98 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
\r
100 text = new StringBuilder (Text, space);
\r
102 this.ending = ending;
\r
105 widths = new float[space + 1];
\r
108 tags = new LineTag(this, 1);
\r
110 tags.Color = color;
\r
113 internal Line (Document document, int LineNo, string Text, LineTag tag, LineEnding ending) : this(document, ending)
\r
115 space = Text.Length > DEFAULT_TEXT_LEN ? Text.Length+1 : DEFAULT_TEXT_LEN;
\r
117 text = new StringBuilder (Text, space);
\r
118 this.ending = ending;
\r
121 widths = new float[space + 1];
\r
125 #endregion // Constructors
\r
127 #region Internal Properties
\r
128 internal HorizontalAlignment Alignment {
\r
129 get { return alignment; }
\r
131 if (alignment != value) {
\r
138 internal int HangingIndent {
\r
139 get { return hanging_indent; }
\r
141 hanging_indent = value;
\r
146 internal int Height {
\r
147 get { return height; }
\r
148 set { height = value; }
\r
151 internal int Indent {
\r
152 get { return indent; }
\r
159 internal int LineNo {
\r
160 get { return line_no; }
\r
161 set { line_no = value; }
\r
164 internal int RightIndent {
\r
165 get { return right_indent; }
\r
167 right_indent = value;
\r
172 internal int Width {
\r
174 int res = (int) widths [text.Length];
\r
179 internal string Text {
\r
180 get { return text.ToString(); }
\r
182 text = new StringBuilder(value, value.Length > DEFAULT_TEXT_LEN ? value.Length : DEFAULT_TEXT_LEN);
\r
188 if (document.multiline)
\r
189 return align_shift;
\r
190 return offset + align_shift;
\r
196 if (!document.multiline)
\r
197 return document.top_margin;
\r
198 return document.top_margin + offset;
\r
201 #endregion // Internal Properties
\r
203 #region Internal Methods
\r
206 /// Builds a simple code to record which tags are links and how many tags
\r
207 /// used to compare lines before and after to see if the scan for links
\r
208 /// process has changed anything.
\r
210 internal void LinkRecord (StringBuilder linkRecord)
\r
212 LineTag tag = tags;
\r
214 while (tag != null) {
\r
216 linkRecord.Append ("L");
\r
218 linkRecord.Append ("N");
\r
225 /// Clears all link properties from tags
\r
227 internal void ClearLinks ()
\r
229 LineTag tag = tags;
\r
231 while (tag != null) {
\r
232 tag.IsLink = false;
\r
237 public void DeleteCharacters(int pos, int count)
\r
240 bool streamline = false;
\r
242 // Can't delete more than the line has
\r
243 if (pos >= text.Length)
\r
246 // Find the first tag that we are deleting from
\r
247 tag = FindTag (pos + 1);
\r
249 // Remove the characters from the line
\r
250 text.Remove (pos, count);
\r
255 // Check if we're crossing tag boundaries
\r
256 if ((pos + count) > (tag.Start + tag.Length - 1)) {
\r
259 // We have to delete cross tag boundaries
\r
263 left -= tag.Start + tag.Length - pos - 1;
\r
266 // Update the start of each tag
\r
267 while ((tag != null) && (left > 0)) {
\r
268 tag.Start -= count - left;
\r
270 if (tag.Length > left) {
\r
273 left -= tag.Length;
\r
279 // We got off easy, same tag
\r
281 if (tag.Length == 0)
\r
285 // Delete empty orphaned tags at the end
\r
286 LineTag walk = tag;
\r
287 while (walk != null && walk.Next != null && walk.Next.Length == 0) {
\r
289 walk.Next = walk.Next.Next;
\r
290 if (walk.Next != null)
\r
291 walk.Next.Previous = t;
\r
295 // Adjust the start point of any tags following
\r
298 while (tag != null) {
\r
299 tag.Start -= count;
\r
307 Streamline (document.Lines);
\r
310 // This doesn't do exactly what you would think, it just pulls off the \n part of the ending
\r
311 internal void DrawEnding (Graphics dc, float y)
\r
313 if (document.multiline)
\r
315 LineTag last = tags;
\r
316 while (last.Next != null)
\r
319 string end_str = null;
\r
320 switch (document.LineEndingLength (ending)) {
\r
324 end_str = "\u0013";
\r
327 end_str = "\u0013\u0013";
\r
330 end_str = "\u0013\u0013\u0013";
\r
334 TextBoxTextRenderer.DrawText (dc, end_str, last.Font, last.Color, X + widths [TextLengthWithoutEnding ()] - document.viewport_x + document.OffsetX, y, true);
\r
337 /// <summary> Find the tag on a line based on the character position, pos is 0-based</summary>
\r
338 internal LineTag FindTag (int pos)
\r
347 if (pos >= text.Length)
\r
348 pos = text.Length - 1;
\r
350 while (tag != null) {
\r
351 if (((tag.Start - 1) <= pos) && (pos <= (tag.Start + tag.Length - 1)))
\r
352 return LineTag.GetFinalTag (tag);
\r
360 public override int GetHashCode ()
\r
362 return base.GetHashCode ();
\r
365 // Get the tag that contains this x coordinate
\r
366 public LineTag GetTag (int x)
\r
368 LineTag tag = tags;
\r
370 // Coord is to the left of the first character
\r
372 return LineTag.GetFinalTag (tag);
\r
374 // All we have is a linked-list of tags, so we have
\r
375 // to do a linear search. But there shouldn't be
\r
376 // too many tags per line in general.
\r
378 if (x >= tag.X && x < (tag.X + tag.Width))
\r
381 if (tag.Next != null)
\r
384 return LineTag.GetFinalTag (tag);
\r
388 // Make sure we always have enoughs space in text and widths
\r
389 internal void Grow (int minimum)
\r
392 float[] new_widths;
\r
394 length = text.Length;
\r
396 if ((length + minimum) > space) {
\r
397 // We need to grow; double the size
\r
399 if ((length + minimum) > (space * 2)) {
\r
400 new_widths = new float[length + minimum * 2 + 1];
\r
401 space = length + minimum * 2;
\r
403 new_widths = new float[space * 2 + 1];
\r
406 widths.CopyTo (new_widths, 0);
\r
408 widths = new_widths;
\r
411 public void InsertString (int pos, string s)
\r
413 InsertString (pos, s, FindTag (pos));
\r
416 // Inserts a string at the given position
\r
417 public void InsertString (int pos, string s, LineTag tag)
\r
419 int len = s.Length;
\r
421 // Insert the text into the StringBuilder
\r
422 text.Insert (pos, s);
\r
424 // Update the start position of every tag after this one
\r
427 while (tag != null) {
\r
432 // Make sure we have room in the widths array
\r
435 // This line needs to be recalculated
\r
440 /// Go through all tags on a line and recalculate all size-related values;
\r
441 /// returns true if lineheight changed
\r
443 internal bool RecalculateLine (Graphics g, Document doc)
\r
459 len = this.text.Length;
\r
461 prev_offset = this.offset; // For drawing optimization calculations
\r
462 prev_height = this.height;
\r
463 prev_ascent = this.ascent;
\r
464 this.height = 0; // Reset line height
\r
465 this.ascent = 0; // Reset the ascent for the line
\r
468 if (ending == LineEnding.Wrap)
\r
469 widths[0] = document.left_margin + hanging_indent;
\r
471 widths[0] = document.left_margin + indent;
\r
473 this.recalc = false;
\r
479 while (pos < len) {
\r
481 while (tag.Length == 0) { // We should always have tags after a tag.length==0 unless len==0
\r
483 tag.Shift = tag.Line.ascent - tag.Ascent;
\r
487 size = tag.SizeOfPosition (g, pos);
\r
490 if (Char.IsWhiteSpace (text[pos]))
\r
491 wrap_pos = pos + 1;
\r
494 if ((wrap_pos > 0) && (wrap_pos != len) && (widths[pos] + w) + 5 > (doc.viewport_width - this.right_indent)) {
\r
495 // Make sure to set the last width of the line before wrapping
\r
496 widths[pos + 1] = widths[pos] + w;
\r
500 doc.Split (this, tag, pos);
\r
501 ending = LineEnding.Wrap;
\r
502 len = this.text.Length;
\r
506 } else if (pos > 1 && (widths[pos] + w) > (doc.viewport_width - this.right_indent)) {
\r
507 // No suitable wrap position was found so break right in the middle of a word
\r
509 // Make sure to set the last width of the line before wrapping
\r
510 widths[pos + 1] = widths[pos] + w;
\r
512 doc.Split (this, tag, pos);
\r
513 ending = LineEnding.Wrap;
\r
514 len = this.text.Length;
\r
520 // Contract all wrapped lines that follow back into our line
\r
524 widths[pos] = widths[pos - 1] + w;
\r
527 line = doc.GetLine (this.line_no + 1);
\r
528 if ((line != null) && (ending == LineEnding.Wrap || ending == LineEnding.None)) {
\r
529 // Pull the two lines together
\r
530 doc.Combine (this.line_no, this.line_no + 1);
\r
531 len = this.text.Length;
\r
537 if (pos == (tag.Start - 1 + tag.Length)) {
\r
538 // We just found the end of our current tag
\r
539 tag.Height = tag.MaxHeight ();
\r
541 // Check if we're the tallest on the line (so far)
\r
542 if (tag.Height > this.height)
\r
543 this.height = tag.Height; // Yep; make sure the line knows
\r
545 if (tag.Ascent > this.ascent) {
\r
548 // We have a tag that has a taller ascent than the line;
\r
550 while (t != null && t != tag) {
\r
551 t.Shift = tag.Ascent - t.Ascent;
\r
555 // Save on our line
\r
556 this.ascent = tag.Ascent;
\r
558 tag.Shift = this.ascent - tag.Ascent;
\r
569 while (tag != null) {
\r
570 tag.Shift = tag.Line.ascent - tag.Ascent;
\r
574 if (this.height == 0) {
\r
575 this.height = tags.Font.Height;
\r
576 tags.Height = this.height;
\r
580 if (prev_offset != offset || prev_height != this.height || prev_ascent != this.ascent)
\r
587 /// Recalculate a single line using the same char for every character in the line
\r
589 internal bool RecalculatePasswordLine (Graphics g, Document doc)
\r
598 len = this.text.Length;
\r
603 this.recalc = false;
\r
604 widths[0] = document.left_margin + indent;
\r
606 w = TextBoxTextRenderer.MeasureText (g, doc.password_char, tags.Font).Width;
\r
608 if (this.height != (int)tag.Font.Height)
\r
613 this.height = (int)tag.Font.Height;
\r
614 tag.Height = this.height;
\r
616 this.ascent = tag.Ascent;
\r
618 while (pos < len) {
\r
620 widths[pos] = widths[pos - 1] + w;
\r
626 internal void Streamline (int lines)
\r
631 current = this.tags;
\r
632 next = current.Next;
\r
635 // Catch what the loop below wont; eliminate 0 length
\r
636 // tags, but only if there are other tags after us
\r
637 // We only eliminate text tags if there is another text tag
\r
638 // after it. Otherwise we wind up trying to type on picture tags
\r
640 while ((current.Length == 0) && (next != null) && (next.IsTextTag)) {
\r
642 tags.Previous = null;
\r
644 next = current.Next;
\r
651 while (next != null) {
\r
652 // Take out 0 length tags unless it's the last tag in the document
\r
653 if (current.IsTextTag && next.Length == 0 && next.IsTextTag) {
\r
654 if ((next.Next != null) || (line_no != lines)) {
\r
655 current.Next = next.Next;
\r
656 if (current.Next != null) {
\r
657 current.Next.Previous = current;
\r
659 next = current.Next;
\r
664 if (current.Combine (next)) {
\r
665 next = current.Next;
\r
669 current = current.Next;
\r
670 next = current.Next;
\r
674 internal int TextLengthWithoutEnding ()
\r
676 return text.Length - document.LineEndingLength (ending);
\r
679 internal string TextWithoutEnding ()
\r
681 return text.ToString (0, text.Length - document.LineEndingLength (ending));
\r
683 #endregion // Internal Methods
\r
685 #region Administrative
\r
686 public object Clone ()
\r
690 clone = new Line (document, ending);
\r
695 clone.left = (Line)left.Clone();
\r
698 clone.left = (Line)left.Clone();
\r
703 internal object CloneLine ()
\r
707 clone = new Line (document, ending);
\r
714 public int CompareTo (object obj)
\r
719 if (! (obj is Line))
\r
720 throw new ArgumentException("Object is not of type Line", "obj");
\r
722 if (line_no < ((Line)obj).line_no)
\r
724 else if (line_no > ((Line)obj).line_no)
\r
730 public override bool Equals (object obj)
\r
735 if (!(obj is Line))
\r
741 if (line_no == ((Line)obj).line_no)
\r
747 public override string ToString()
\r
749 return string.Format ("Line {0}", line_no);
\r
751 #endregion // Administrative
\r