// 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 // // using System; using System.Collections; using System.Drawing; using System.Drawing.Text; using System.Text; namespace ShiftUI { internal class LineTag { #region Local Variables // Formatting private Font font; // System.Drawing.Font object for this tag private Color color; // The font color for this tag private Color back_color; // In 2.0 tags can have background colours. private Font link_font; // Cached font used for link if IsLink private bool is_link; // Whether this tag is a link private string link_text; // The full link text e.g. this might be // word-wrapped to "w" but this would be // "www.mono-project.com" // Payload; text private int start; // start, in chars; index into Line.text // 1 based!! // Drawing support private int height; // Height in pixels of the text this tag describes private int ascent; // Ascent of the font for this tag private int descent; // Descent of the font for this tag private int shift; // Shift down for this tag, to stay on baseline // Administrative private Line line; // The line we're on private LineTag next; // Next tag on the same line private LineTag previous; // Previous tag on the same line #endregion #region Constructors public LineTag (Line line, int start) { this.line = line; Start = start; link_font = null; is_link = false; link_text = null; } #endregion // Constructors #region Public Properties public int Ascent { get { return ascent; } } public Color BackColor { get { return back_color; } set { back_color = value; } } public Color ColorToDisplay { get { if (IsLink == true) return Color.Blue; return color; } } public Color Color { get { return color; } set { color = value; } } public int Descent { get { return descent; } } public int End { get { return start + Length; } } public Font FontToDisplay { get { if (IsLink) { if (link_font == null) link_font = new Font (font.FontFamily, font.Size, font.Style | FontStyle.Underline); return link_font; } return font; } } public Font Font { get { return font; } set { if (font != value) { link_font = null; font = value; height = Font.Height; XplatUI.GetFontMetrics (Hwnd.GraphicsContext, Font, out ascent, out descent); line.recalc = true; } } } public int Height { get { return height; } set { height = value; } } public virtual bool IsTextTag { get { return true; } } public int Length { get { int res = 0; if (next != null) res = next.start - start; else res = line.text.Length - (start - 1); return res > 0 ? res : 0; } } public Line Line { get { return line; } set { line = value; } } public LineTag Next { get { return next; } set { next = value; } } public LineTag Previous { get { return previous; } set { previous = value; } } public int Shift { get { return shift; } set { shift = value; } } public int Start { get { return start; } set { #if DEBUG if (value <= 0) throw new Exception("Start of tag must be 1 or higher!"); if (this.Previous != null) { if (this.Previous.Start == value) System.Console.Write("Creating empty tag"); if (this.Previous.Start > value) throw new Exception("New tag makes an insane tag"); } #endif start = value; } } public int TextEnd { get { return start + TextLength; } } public int TextLength { get { int res = 0; if (next != null) res = next.start - start; else res = line.TextLengthWithoutEnding () - (start - 1); return res > 0 ? res : 0; } } public float Width { get { if (Length == 0) return 0; return line.widths [start + Length - 1] - (start != 0 ? line.widths [start - 1] : 0); } } public float X { get { if (start == 0) return line.X; return line.X + line.widths [start - 1]; } } public bool IsLink { get { return is_link; } set { is_link = value; } } public string LinkText { get { return link_text; } set { link_text = value; } } #endregion #region Public Methods ///Break a tag into two with identical attributes; pos is 1-based; returns tag starting at >pos< or null if end-of-line public LineTag Break (int pos) { LineTag new_tag; #if DEBUG // Sanity if (pos < this.Start) throw new Exception ("Breaking at a negative point"); #endif #if DEBUG if (pos > End) throw new Exception ("Breaking past the end of a line"); #endif new_tag = new LineTag(line, pos); new_tag.CopyFormattingFrom (this); new_tag.next = this.next; this.next = new_tag; new_tag.previous = this; if (new_tag.next != null) new_tag.next.previous = new_tag; return new_tag; } /// Combines 'this' tag with 'other' tag public bool Combine (LineTag other) { if (!this.Equals (other)) return false; this.next = other.next; if (this.next != null) this.next.previous = this; return true; } public void CopyFormattingFrom (LineTag other) { Font = other.font; color = other.color; back_color = other.back_color; } public void Delete () { // If we are the only tag, we can't be deleted if (previous == null && next == null) return; // If we are the last tag, deletion is easy if (next == null) { previous.next = null; return; } // Easy cases gone, little tougher, delete ourself // Update links, and start next.previous = null; LineTag loop = next; while (loop != null) { loop.Start -= Length; loop = loop.next; } return; } public virtual void Draw (Graphics dc, Color color, float x, float y, int start, int end) { TextBoxTextRenderer.DrawText (dc, line.text.ToString (start, end).Replace ("\r", string.Empty), FontToDisplay, color, x, y, false); } public virtual void Draw (Graphics dc, Color color, float xoff, float y, int start, int end, string text) { Rectangle measured_text; Draw (dc, color, xoff, y, start, end, text, out measured_text, false); } /// /// /// /// 0 based start index public virtual void Draw (Graphics dc, Color color, float xoff, float y, int drawStart, int drawEnd, string text, out Rectangle measuredText, bool measureText) { if (measureText) { int xstart = (int)line.widths [drawStart] + (int)xoff; int xend = (int)line.widths [drawEnd] - (int)line.widths [drawStart]; int ystart = (int)y; int yend = (int)TextBoxTextRenderer.MeasureText (dc, Text (), FontToDisplay).Height; measuredText = new Rectangle (xstart, ystart, xend, yend); } else { measuredText = new Rectangle (); } while (drawStart < drawEnd) { int tab_index = text.IndexOf ("\t", drawStart); if (tab_index == -1) tab_index = drawEnd; TextBoxTextRenderer.DrawText (dc, text.Substring (drawStart, tab_index - drawStart).Replace ("\r", string.Empty), FontToDisplay, color, xoff + line.widths [drawStart], y, false); // non multilines get the unknown char if (!line.document.multiline && tab_index != drawEnd) TextBoxTextRenderer.DrawText (dc, "\u0013", FontToDisplay, color, xoff + line.widths [tab_index], y, true); drawStart = tab_index + 1; } } /// Checks if 'this' tag describes the same formatting options as 'obj' public override bool Equals (object obj) { LineTag other; if (obj == null) return false; if (!(obj is LineTag)) return false; if (obj == this) return true; other = (LineTag)obj; if (other.IsTextTag != IsTextTag) return false; if (this.IsLink != other.IsLink) return false; if (this.LinkText != other.LinkText) return false; if (this.font.Equals (other.font) && this.color.Equals (other.color)) return true; return false; } /// Finds the tag that describes the character at position 'pos' (0 based) on 'line' public static LineTag FindTag (Line line, int pos) { LineTag tag = line.tags; // Beginning of line is a bit special if (pos == 0) return tag; // Not sure if we should get the final tag here while (tag != null) { // [H e][l][l o _ W][o r] Text // [1 2][3][4 5 6 7][8 9] Start // 3 4 8 10 End // 0 1 2 3 4 5 6 7 8 9 Pos if ((tag.start <= pos) && (pos < tag.End)) return GetFinalTag (tag); tag = tag.next; } return null; } /// Applies 'font' and 'brush' to characters starting at 'start' for 'length' chars; /// Removes any previous tags overlapping the same area; /// returns true if lineheight has changed /// 1-based character position on line public static bool FormatText (Line line, int formatStart, int length, Font font, Color color, Color backColor, FormatSpecified specified) { LineTag tag; LineTag start_tag; LineTag end_tag; int end; bool retval = false; // Assume line-height doesn't change // Too simple? if (((FormatSpecified.Font & specified) == FormatSpecified.Font) && font.Height != line.height) retval = true; line.recalc = true; // This forces recalculation of the line in RecalculateDocument // A little sanity, not sure if it's needed, might be able to remove for speed if (length > line.text.Length) length = line.text.Length; tag = line.tags; end = formatStart + length; // Common special case if ((formatStart == 1) && (length == tag.Length)) { SetFormat (tag, font, color, backColor, specified); return retval; } // empty selection style at begining of line means // we only need one new tag if (formatStart == 1 && length == 0) { line.tags.Break (1); SetFormat (line.tags, font, color, backColor, specified); return retval; } start_tag = FindTag (line, formatStart - 1); // we are at an empty tag already! // e.g. [Tag 0 - "He"][Tag 1 = 0 length][Tag 2 "llo world"] // Find Tag will return tag 0 at position 3, but we should just // use the empty tag after.. if (start_tag.End == formatStart && length == 0 && start_tag.Next != null && start_tag.Next.Length == 0) { SetFormat (start_tag.Next, font, color, backColor, specified); return retval; } // if we are at the end of a tag, we want to move to the next tag while (start_tag.End == formatStart && start_tag.Next != null) start_tag = start_tag.Next; tag = start_tag.Break (formatStart); // empty selection style at end of line - its the only situation // where the rest of the tag would be empty, since we moved to the // begining of next non empty tag if (tag.Length == 0) { SetFormat (tag, font, color, backColor, specified); return retval; } // empty - so we just create another tag for // after our new (now) empty one.. if (length == 0) { tag.Break (formatStart); SetFormat (tag, font, color, backColor, specified); return retval; } while (tag != null && tag.End <= end) { SetFormat (tag, font, color, backColor, specified); tag = tag.next; } // did the last tag conveniently fit? if (tag != null && tag.End == end) return retval; /// Now do the last tag end_tag = FindTag (line, end-1); if (end_tag != null) { end_tag.Break (end); SetFormat (end_tag, font, color, backColor, specified); } return retval; } // Gets the character at the x-coordinate. Index is based from the // line, not the start of the tag. // returns 0 based index (0 means before character at 1, 1 means at character 1) public int GetCharIndex (int x) { int low = start; int high = low + Length; int length_no_ending = line.TextLengthWithoutEnding (); if (Length == 0) return low-1; if (length_no_ending == 0) return 0; if (x < line.widths [low]) { if (low == 1 && x > (line.widths [1] / 2)) return low; return low - 1; } if (x > line.widths[length_no_ending]) return length_no_ending; while (low < high - 1) { int mid = (high + low) / 2; float width = line.widths[mid]; if (width < x) low = mid; else high = mid; } float char_width = line.widths[high] - line.widths[low]; if ((x - line.widths[low]) >= (char_width / 2)) return high; else return low; } // There can be multiple tags at the same position, we want to make // sure we are using the very last tag at the given position // Empty tags are necessary if style is set at a position with // no length. public static LineTag GetFinalTag (LineTag tag) { LineTag res = tag; while (res.Length == 0 && res.next != null && res.next.Length == 0) res = res.next; return res; } public override int GetHashCode () { return base.GetHashCode (); } internal virtual int MaxHeight () { return font.Height; } private static void SetFormat (LineTag tag, Font font, Color color, Color back_color, FormatSpecified specified) { if ((FormatSpecified.Font & specified) == FormatSpecified.Font) { tag.Font = font; } if ((FormatSpecified.Color & specified) == FormatSpecified.Color) tag.color = color; if ((FormatSpecified.BackColor & specified) == FormatSpecified.BackColor) { tag.back_color = back_color; } // Console.WriteLine ("setting format: {0} {1} new color {2}", color.Color, specified, tag.color.Color); } public virtual SizeF SizeOfPosition (Graphics dc, int pos) { if (pos >= line.TextLengthWithoutEnding () && line.document.multiline) return SizeF.Empty; string text = line.text.ToString (pos, 1); switch ((int) text [0]) { case '\t': if (!line.document.multiline) goto case 10; SizeF res = TextBoxTextRenderer.MeasureText (dc, " ", font); res.Width *= 8.0F; return res; case 10: case 13: return TextBoxTextRenderer.MeasureText (dc, "\u000D", font); } return TextBoxTextRenderer.MeasureText (dc, text, font); } public virtual string Text () { return line.text.ToString (start - 1, Length); } public override string ToString () { if (Length > 0) return string.Format ("{0} Tag starts at index: {1}, length: {2}, text: {3}, font: {4}", GetType (), start, Length, Text (), font.ToString ()); return string.Format ("Zero Length tag at index: {0}", start); } #endregion // Internal Methods } }