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 LineTag
\r
37 #region Local Variables
\r
39 private Font font; // System.Drawing.Font object for this tag
\r
40 private Color color; // The font color for this tag
\r
41 private Color back_color; // In 2.0 tags can have background colours.
\r
42 private Font link_font; // Cached font used for link if IsLink
\r
43 private bool is_link; // Whether this tag is a link
\r
44 private string link_text; // The full link text e.g. this might be
\r
45 // word-wrapped to "w" but this would be
\r
46 // "www.mono-project.com"
\r
49 private int start; // start, in chars; index into Line.text
\r
53 private int height; // Height in pixels of the text this tag describes
\r
54 private int ascent; // Ascent of the font for this tag
\r
55 private int descent; // Descent of the font for this tag
\r
56 private int shift; // Shift down for this tag, to stay on baseline
\r
59 private Line line; // The line we're on
\r
60 private LineTag next; // Next tag on the same line
\r
61 private LineTag previous; // Previous tag on the same line
\r
64 #region Constructors
\r
65 public LineTag (Line line, int start)
\r
73 #endregion // Constructors
\r
75 #region Public Properties
\r
77 get { return ascent; }
\r
80 public Color BackColor {
\r
81 get { return back_color; }
\r
82 set { back_color = value; }
\r
85 public Color ColorToDisplay {
\r
94 public Color Color {
\r
95 get { return color; }
\r
96 set { color = value; }
\r
99 public int Descent {
\r
100 get { return descent; }
\r
104 get { return start + Length; }
\r
107 public Font FontToDisplay {
\r
110 if (link_font == null)
\r
111 link_font = new Font (font.FontFamily, font.Size, font.Style | FontStyle.Underline);
\r
121 get { return font; }
\r
123 if (font != value) {
\r
127 height = Font.Height;
\r
128 XplatUI.GetFontMetrics (Hwnd.GraphicsContext, Font, out ascent, out descent);
\r
129 line.recalc = true;
\r
134 public int Height {
\r
135 get { return height; }
\r
136 set { height = value; }
\r
139 public virtual bool IsTextTag {
\r
140 get { return true; }
\r
143 public int Length {
\r
147 res = next.start - start;
\r
149 res = line.text.Length - (start - 1);
\r
151 return res > 0 ? res : 0;
\r
156 get { return line; }
\r
157 set { line = value; }
\r
160 public LineTag Next {
\r
161 get { return next; }
\r
162 set { next = value; }
\r
165 public LineTag Previous {
\r
166 get { return previous; }
\r
167 set { previous = value; }
\r
171 get { return shift; }
\r
172 set { shift = value; }
\r
176 get { return start; }
\r
180 throw new Exception("Start of tag must be 1 or higher!");
\r
182 if (this.Previous != null) {
\r
183 if (this.Previous.Start == value)
\r
184 System.Console.Write("Creating empty tag");
\r
185 if (this.Previous.Start > value)
\r
186 throw new Exception("New tag makes an insane tag");
\r
193 public int TextEnd {
\r
194 get { return start + TextLength; }
\r
197 public int TextLength {
\r
201 res = next.start - start;
\r
203 res = line.TextLengthWithoutEnding () - (start - 1);
\r
205 return res > 0 ? res : 0;
\r
209 public float Width {
\r
213 return line.widths [start + Length - 1] - (start != 0 ? line.widths [start - 1] : 0);
\r
221 return line.X + line.widths [start - 1];
\r
225 public bool IsLink {
\r
226 get { return is_link; }
\r
227 set { is_link = value; }
\r
230 public string LinkText {
\r
231 get { return link_text; }
\r
232 set { link_text = value; }
\r
236 #region Public Methods
\r
237 ///<summary>Break a tag into two with identical attributes; pos is 1-based; returns tag starting at >pos< or null if end-of-line</summary>
\r
238 public LineTag Break (int pos)
\r
244 if (pos < this.Start)
\r
245 throw new Exception ("Breaking at a negative point");
\r
250 throw new Exception ("Breaking past the end of a line");
\r
253 new_tag = new LineTag(line, pos);
\r
254 new_tag.CopyFormattingFrom (this);
\r
256 new_tag.next = this.next;
\r
257 this.next = new_tag;
\r
258 new_tag.previous = this;
\r
260 if (new_tag.next != null)
\r
261 new_tag.next.previous = new_tag;
\r
266 /// <summary>Combines 'this' tag with 'other' tag</summary>
\r
267 public bool Combine (LineTag other)
\r
269 if (!this.Equals (other))
\r
272 this.next = other.next;
\r
274 if (this.next != null)
\r
275 this.next.previous = this;
\r
280 public void CopyFormattingFrom (LineTag other)
\r
283 color = other.color;
\r
284 back_color = other.back_color;
\r
287 public void Delete ()
\r
289 // If we are the only tag, we can't be deleted
\r
290 if (previous == null && next == null)
\r
293 // If we are the last tag, deletion is easy
\r
294 if (next == null) {
\r
295 previous.next = null;
\r
299 // Easy cases gone, little tougher, delete ourself
\r
300 // Update links, and start
\r
301 next.previous = null;
\r
303 LineTag loop = next;
\r
305 while (loop != null) {
\r
306 loop.Start -= Length;
\r
313 public virtual void Draw (Graphics dc, Color color, float x, float y, int start, int end)
\r
315 TextBoxTextRenderer.DrawText (dc, line.text.ToString (start, end).Replace ("\r", string.Empty), FontToDisplay, color, x, y, false);
\r
318 public virtual void Draw (Graphics dc, Color color, float xoff, float y, int start, int end, string text)
\r
320 Rectangle measured_text;
\r
321 Draw (dc, color, xoff, y, start, end, text, out measured_text, false);
\r
327 /// <param name="drawStart">0 based start index</param>
\r
328 public virtual void Draw (Graphics dc, Color color, float xoff, float y, int drawStart, int drawEnd,
\r
329 string text, out Rectangle measuredText, bool measureText)
\r
332 int xstart = (int)line.widths [drawStart] + (int)xoff;
\r
333 int xend = (int)line.widths [drawEnd] - (int)line.widths [drawStart];
\r
334 int ystart = (int)y;
\r
335 int yend = (int)TextBoxTextRenderer.MeasureText (dc, Text (), FontToDisplay).Height;
\r
337 measuredText = new Rectangle (xstart, ystart, xend, yend);
\r
339 measuredText = new Rectangle ();
\r
342 while (drawStart < drawEnd) {
\r
343 int tab_index = text.IndexOf ("\t", drawStart);
\r
345 if (tab_index == -1)
\r
346 tab_index = drawEnd;
\r
348 TextBoxTextRenderer.DrawText (dc, text.Substring (drawStart, tab_index - drawStart).Replace ("\r", string.Empty), FontToDisplay, color, xoff + line.widths [drawStart], y, false);
\r
350 // non multilines get the unknown char
\r
351 if (!line.document.multiline && tab_index != drawEnd)
\r
352 TextBoxTextRenderer.DrawText (dc, "\u0013", FontToDisplay, color, xoff + line.widths [tab_index], y, true);
\r
354 drawStart = tab_index + 1;
\r
358 /// <summary>Checks if 'this' tag describes the same formatting options as 'obj'</summary>
\r
359 public override bool Equals (object obj)
\r
366 if (!(obj is LineTag))
\r
372 other = (LineTag)obj;
\r
374 if (other.IsTextTag != IsTextTag)
\r
377 if (this.IsLink != other.IsLink)
\r
380 if (this.LinkText != other.LinkText)
\r
383 if (this.font.Equals (other.font) && this.color.Equals (other.color))
\r
389 /// <summary>Finds the tag that describes the character at position 'pos' (0 based) on 'line'</summary>
\r
390 public static LineTag FindTag (Line line, int pos)
\r
392 LineTag tag = line.tags;
\r
394 // Beginning of line is a bit special
\r
396 return tag; // Not sure if we should get the final tag here
\r
398 while (tag != null) {
\r
399 // [H e][l][l o _ W][o r] Text
\r
400 // [1 2][3][4 5 6 7][8 9] Start
\r
402 // 0 1 2 3 4 5 6 7 8 9 Pos
\r
403 if ((tag.start <= pos) && (pos < tag.End))
\r
404 return GetFinalTag (tag);
\r
412 /// <summary>Applies 'font' and 'brush' to characters starting at 'start' for 'length' chars;
\r
413 /// Removes any previous tags overlapping the same area;
\r
414 /// returns true if lineheight has changed</summary>
\r
415 /// <param name="formatStart">1-based character position on line</param>
\r
416 public static bool FormatText (Line line, int formatStart, int length, Font font, Color color, Color backColor, FormatSpecified specified)
\r
422 bool retval = false; // Assume line-height doesn't change
\r
425 if (((FormatSpecified.Font & specified) == FormatSpecified.Font) && font.Height != line.height)
\r
428 line.recalc = true; // This forces recalculation of the line in RecalculateDocument
\r
430 // A little sanity, not sure if it's needed, might be able to remove for speed
\r
431 if (length > line.text.Length)
\r
432 length = line.text.Length;
\r
435 end = formatStart + length;
\r
437 // Common special case
\r
438 if ((formatStart == 1) && (length == tag.Length)) {
\r
439 SetFormat (tag, font, color, backColor, specified);
\r
443 // empty selection style at begining of line means
\r
444 // we only need one new tag
\r
445 if (formatStart == 1 && length == 0) {
\r
446 line.tags.Break (1);
\r
447 SetFormat (line.tags, font, color, backColor, specified);
\r
451 start_tag = FindTag (line, formatStart - 1);
\r
453 // we are at an empty tag already!
\r
454 // e.g. [Tag 0 - "He"][Tag 1 = 0 length][Tag 2 "llo world"]
\r
455 // Find Tag will return tag 0 at position 3, but we should just
\r
456 // use the empty tag after..
\r
457 if (start_tag.End == formatStart && length == 0 && start_tag.Next != null && start_tag.Next.Length == 0) {
\r
458 SetFormat (start_tag.Next, font, color, backColor, specified);
\r
462 // if we are at the end of a tag, we want to move to the next tag
\r
463 while (start_tag.End == formatStart && start_tag.Next != null)
\r
464 start_tag = start_tag.Next;
\r
466 tag = start_tag.Break (formatStart);
\r
468 // empty selection style at end of line - its the only situation
\r
469 // where the rest of the tag would be empty, since we moved to the
\r
470 // begining of next non empty tag
\r
471 if (tag.Length == 0) {
\r
472 SetFormat (tag, font, color, backColor, specified);
\r
476 // empty - so we just create another tag for
\r
477 // after our new (now) empty one..
\r
479 tag.Break (formatStart);
\r
480 SetFormat (tag, font, color, backColor, specified);
\r
484 while (tag != null && tag.End <= end) {
\r
485 SetFormat (tag, font, color, backColor, specified);
\r
489 // did the last tag conveniently fit?
\r
490 if (tag != null && tag.End == end)
\r
493 /// Now do the last tag
\r
494 end_tag = FindTag (line, end-1);
\r
496 if (end_tag != null) {
\r
497 end_tag.Break (end);
\r
498 SetFormat (end_tag, font, color, backColor, specified);
\r
504 // Gets the character at the x-coordinate. Index is based from the
\r
505 // line, not the start of the tag.
\r
506 // returns 0 based index (0 means before character at 1, 1 means at character 1)
\r
507 public int GetCharIndex (int x)
\r
510 int high = low + Length;
\r
511 int length_no_ending = line.TextLengthWithoutEnding ();
\r
516 if (length_no_ending == 0)
\r
519 if (x < line.widths [low]) {
\r
520 if (low == 1 && x > (line.widths [1] / 2))
\r
525 if (x > line.widths[length_no_ending])
\r
526 return length_no_ending;
\r
528 while (low < high - 1) {
\r
529 int mid = (high + low) / 2;
\r
530 float width = line.widths[mid];
\r
538 float char_width = line.widths[high] - line.widths[low];
\r
540 if ((x - line.widths[low]) >= (char_width / 2))
\r
546 // There can be multiple tags at the same position, we want to make
\r
547 // sure we are using the very last tag at the given position
\r
548 // Empty tags are necessary if style is set at a position with
\r
550 public static LineTag GetFinalTag (LineTag tag)
\r
554 while (res.Length == 0 && res.next != null && res.next.Length == 0)
\r
560 public override int GetHashCode ()
\r
562 return base.GetHashCode ();
\r
565 internal virtual int MaxHeight ()
\r
567 return font.Height;
\r
570 private static void SetFormat (LineTag tag, Font font, Color color, Color back_color, FormatSpecified specified)
\r
572 if ((FormatSpecified.Font & specified) == FormatSpecified.Font) {
\r
575 if ((FormatSpecified.Color & specified) == FormatSpecified.Color)
\r
577 if ((FormatSpecified.BackColor & specified) == FormatSpecified.BackColor) {
\r
578 tag.back_color = back_color;
\r
580 // Console.WriteLine ("setting format: {0} {1} new color {2}", color.Color, specified, tag.color.Color);
\r
583 public virtual SizeF SizeOfPosition (Graphics dc, int pos)
\r
585 if (pos >= line.TextLengthWithoutEnding () && line.document.multiline)
\r
586 return SizeF.Empty;
\r
588 string text = line.text.ToString (pos, 1);
\r
589 switch ((int) text [0]) {
\r
591 if (!line.document.multiline)
\r
593 SizeF res = TextBoxTextRenderer.MeasureText (dc, " ", font);
\r
598 return TextBoxTextRenderer.MeasureText (dc, "\u000D", font);
\r
601 return TextBoxTextRenderer.MeasureText (dc, text, font);
\r
604 public virtual string Text ()
\r
606 return line.text.ToString (start - 1, Length);
\r
609 public override string ToString ()
\r
612 return string.Format ("{0} Tag starts at index: {1}, length: {2}, text: {3}, font: {4}", GetType (), start, Length, Text (), font.ToString ());
\r
614 return string.Format ("Zero Length tag at index: {0}", start);
\r
616 #endregion // Internal Methods
\r