/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections.Generic; using System.Drawing; using System.Collections; using System.Diagnostics; using JetBrains.Annotations; using JetBrains.UI.Interop; namespace JetBrains.UI.RichText { #region RichTextParameters structure /// /// Contains parameters for a whole rich text block /// public class RichTextParameters { /// /// Font to use /// private Font myFont; /// /// Default text style /// private TextStyle myStyle; /// /// Gets or sets used font /// public Font Font { get { return myFont; } set { myFont = value; } } /// /// Gets or sets default text style /// public TextStyle Style { get { return myStyle; } set { myStyle = value; } } /// /// Creates new rich text parameters /// public RichTextParameters() { } /// /// Creates new rich text parameters /// /// Font to use public RichTextParameters( Font font ) { myFont = font; myStyle = TextStyle.DefaultStyle; } /// /// Creates new rich text parameters /// /// Font to use /// Default text style public RichTextParameters( Font font, TextStyle style ) { myFont = font; myStyle = style; } } #endregion /// /// Represents a formatted text block (i.e., actually, sequence of instances). /// public class RichText : ICloneable { private class TextRangeDataRecord { private int myStartOffset; private int myEndOffset; private object myObject; public int StartOffset { get { return myStartOffset; } } public int EndOffset { get { return myEndOffset; } } public object Object { get { return myObject; } } public TextRangeDataRecord( int startOffset, int endOffset, object @object ) { myStartOffset = startOffset; myEndOffset = endOffset; myObject = @object; } } #region Fields /// /// Text formatting options /// private RichTextParameters myParameters; /// /// Parts of the text /// private ArrayList myParts = new ArrayList( 1 ); /// /// String contents /// private string myString = ""; /// /// Keeps user data attached to ranges /// private ArrayList myData = new ArrayList(); #endregion #region Properties /// /// Gets or sets used parameters /// public RichTextParameters Parameters { get { return myParameters; } set { myParameters = value; } } #endregion #region Size cache private SizeF mySize = SizeF.Empty; private bool mySizeIsValid = false; #endregion #region User data public void PutUserData( int startOffset, int endOffset, object data ) { myData.Add( new TextRangeDataRecord( startOffset, endOffset, data ) ); } public object[] GetUserData( int startOffset, int endOffset ) { ArrayList data = new ArrayList(); foreach( TextRangeDataRecord textRangeDataRecord in myData ) { if (textRangeDataRecord.StartOffset <= startOffset && textRangeDataRecord.EndOffset >= endOffset) data.Add( textRangeDataRecord.Object ); } return (object[]) data.ToArray( typeof (object) ); } #endregion /// /// Creates a new rich text block /// /// Text content /// Parameters to use public RichText( string s, RichTextParameters parameters ) { myParameters = parameters; if (s != null && s.Length > 0) { myString = s; myParts.Add( new RichString( 0, s.Length, parameters.Style, this ) ); } } private RichText( string s, RichTextParameters parameters, IList parts ) { myParameters = parameters; myString = s; myParts = new ArrayList( parts.Count ); for (int i = 0; i < parts.Count; i++) myParts.Add( ((RichString) parts[ i ]).Clone() ); } /// /// Adds a part with a custom style to the text /// /// The part to add /// The style of the part to add public void Append( string s, TextStyle style ) { myParts.Add( new RichString( myString.Length, s.Length, style, this ) ); myString += s; mySizeIsValid = false; } /// /// Adds part to the text /// /// The part to add public void Append( string s ) { myParts.Add( new RichString( myString.Length, s.Length, myParameters.Style, this ) ); myString += s; mySizeIsValid = false; } /// /// Appends one rich text to another /// /// The rich text to append public void Append( RichText richText ) { foreach( RichString part in richText.myParts ) myParts.Add( part ); myString += richText.myString; mySizeIsValid = false; } /// /// Gets plain string representation of rich text /// /// Plain string representation of rich text public override string ToString() { string s = ""; for (int i = 0; i < myParts.Count; i++) { if (i > 0) s += "|"; s += ((RichString) myParts[ i ]).PartText; } return s; } /// /// Gets the underlying text /// public string Text { get { return myString; } } /// /// Gets total length of the text in characters /// public int Length { get { return myString.Length; } } public int GetCharByOffset( int x, IntPtr hDC ) { if (x < 0) return -1; int currentX = 0; int currentChar = 0; foreach( RichString part in myParts ) { SizeF size = part.GetSize( hDC, myParameters ); if (currentX + (int) size.Width > x) return currentChar + part.GetSymbolByOffset( x - currentX, myParameters, hDC ); currentX += (int) size.Width; currentChar += part.Length; } return -1; } /// /// Gets size of the text in the given graphics /// /// The device context to calculate size in /// Size of the string when drawn in a given graphics /// g is null. public SizeF GetSize( IntPtr hdc ) { if (!mySizeIsValid) { float width = 0, height = 0; foreach( RichString s in myParts ) { SizeF size = s.GetSize( hdc, myParameters ); width += size.Width; height = Math.Max( height, size.Height ); } mySize = new SizeF( width, height ); mySizeIsValid = true; } return mySize; } /// /// Get the size on the screen HDC /// /// public Size GetSize() { IntPtr hDC = Win32Declarations.GetDC( IntPtr.Zero ); try { return GetSize( hDC ).ToSize(); } finally { Win32Declarations.ReleaseDC( IntPtr.Zero, hDC ); } } /// /// Draws the formatted string on a given device contect (HDC) /// /// The device context to draw the string in. /// The rectangle where the string is drawn. /// g is null public void Draw( IntPtr hdc, Rectangle rect ) { foreach( RichString s in myParts ) { int stringWidth = s.Draw( hdc, rect, myParameters ); rect = new Rectangle( rect.Left + stringWidth, rect.Top, rect.Width - stringWidth, rect.Height ); } } /// /// Draws the formatted string on a given graphics /// /// /// The rectangle where the string is drawn. /// g is null public void Draw( Graphics graphics, Rectangle rect ) { IntPtr hdc = graphics.GetHdc(); try { Draw( hdc, rect ); } finally { graphics.ReleaseHdc( hdc ); } } /// /// Draws the formatted string on a given graphics, clipping the text by the /// ClipBounds set on the Graphics. /// /// /// The rectangle where the string is drawn. /// g is null public void DrawClipped( Graphics graphics, Rectangle rect ) { RectangleF rcClip = graphics.ClipBounds; IntPtr hdc = graphics.GetHdc(); try { IntPtr clipRgn = Win32Declarations.CreateRectRgn( 0, 0, 0, 0 ); if (Win32Declarations.GetClipRgn( hdc, clipRgn ) != 1) { Win32Declarations.DeleteObject( clipRgn ); clipRgn = IntPtr.Zero; } Win32Declarations.IntersectClipRect( hdc, (int) rcClip.Left, (int) rcClip.Top, (int) rcClip.Right, (int) rcClip.Bottom ); Draw( hdc, rect ); Win32Declarations.SelectClipRgn( hdc, clipRgn ); Win32Declarations.DeleteObject( clipRgn ); } finally { graphics.ReleaseHdc( hdc ); } } /// /// Sets new colors for the whole text /// /// Foreground color to set /// Background color to set public void SetColors( Color foreColor, Color backColor ) { foreach( RichString s in myParts ) { TextStyle style = s.Style; style.BackgroundColor = backColor; style.ForegroundColor = foreColor; style.EffectColor = foreColor; s.Style = style; } mySizeIsValid = false; } public void SetColors( Color foreColor, Color backColor, int startOffset, int length ) { foreach( RichString s in GetStringsInRange( startOffset, length ) ) { TextStyle style = s.Style; style.BackgroundColor = backColor; style.ForegroundColor = foreColor; style.EffectColor = foreColor; s.Style = style; } mySizeIsValid = false; } /// /// Sets new style to a specified part of the text /// /// The style to set /// Start offset of the block /// Block length /// startOffset is invalid in current string /// length is invalid starting with specified startOffset public void SetStyle( TextStyle style, int startOffset, int length ) { foreach( RichString s in GetStringsInRange( startOffset, length ) ) { s.Style = style; } mySizeIsValid = false; } /// /// Sets new font style to a specified part of the text /// /// The font style to set /// Start offset of the block /// Block length /// startOffset is invalid in current string /// length is invalid starting with specified startOffset public void SetStyle( FontStyle style, int startOffset, int length ) { foreach( RichString s in GetStringsInRange( startOffset, length ) ) { s.Style = new TextStyle( style, s.Style.ForegroundColor, s.Style.BackgroundColor, s.Style.Effect, s.Style.EffectColor ); } mySizeIsValid = false; } /// /// Sets new effect to a specified part of the text /// /// The effect style to set /// The effect color to set /// Start offset of the block /// Block length /// startOffset is invalid in current string /// length is invalid starting with specified startOffset public void SetStyle( TextStyle.EffectStyle effect, Color effectColor, int startOffset, int length ) { foreach( RichString s in GetStringsInRange( startOffset, length ) ) { s.Style = new TextStyle( s.Style.FontStyle, s.Style.ForegroundColor, s.Style.BackgroundColor, effect, effectColor ); } mySizeIsValid = false; } /// /// Splits rich text at the specified offset /// /// The offset to split the text at /// Array of the result parts public RichText[] Split( int offset ) { ArrayList firstPart = new ArrayList(); ArrayList secondPart = new ArrayList(); foreach( RichString s in myParts ) { if (s.Offset + s.Length <= offset) firstPart.Add( s ); else if (s.Offset >= offset) secondPart.Add( s ); else { RichString[] parts = BreakString( s, offset - s.Offset, false ); firstPart.Add( parts[ 0 ] ); secondPart.Add( parts[ 1 ] ); } } return new RichText[] { CreateRichTextFromParts( (RichString[]) firstPart.ToArray( typeof (RichString) ) ), CreateRichTextFromParts( (RichString[]) secondPart.ToArray( typeof (RichString) ) ) }; } #region Private methods private RichText CreateRichTextFromParts( RichString[] parts ) { if (parts.Length == 0) return new RichText( "", myParameters ); string text = ""; int startOffset = parts[ 0 ].Offset; foreach( RichString s in parts ) text += Text.Substring( s.Offset, s.Length ); RichText result = new RichText( text, myParameters ); foreach( RichString s in parts ) result.SetStyle( s.Style, s.Offset - startOffset, s.Length ); return result; } /// /// Gets array of parts which lie in the specified range /// private RichString[] GetStringsInRange( int startOffset, int length ) { if (startOffset < 0) startOffset = 0; if (startOffset + length > myString.Length) length = myString.Length - startOffset; ArrayList strings = new ArrayList(); int offset = startOffset; int endOffset = startOffset + length; while (offset < endOffset) { RichString s = GetPartByOffset( offset ); Debug.Assert( s != null, "We've fixed the offsets so we should always find the corresponding string" ); int startingOffset = GetStartingOffset( s ); int localStartingOffset = offset - startingOffset; if (localStartingOffset > 0) { s = BreakString( s, localStartingOffset, true )[ 1 ]; localStartingOffset = 0; } int localEndingOffset = localStartingOffset + length; if (localEndingOffset < s.Length) { s = BreakString( s, localEndingOffset, true )[ 0 ]; localEndingOffset = s.Length; } strings.Add( s ); offset += s.Length; } return (RichString[]) strings.ToArray( typeof (RichString) ); } /// /// Gets starting offset of a given part in global coordinates /// private int GetStartingOffset( RichString part ) { int currentOffset = 0; foreach( RichString s in myParts ) { if (s == part) return currentOffset; currentOffset += s.Length; } throw new ArgumentException( "There's no such part in this text" ); } /// /// Breaks a part into two parts at given offset /// private RichString[] BreakString( RichString s, int offset, bool insertParts ) { if (offset >= s.Length) return null; RichString[] parts = new RichString[2]; parts[ 0 ] = new RichString( s.Offset, offset, s.Style, this ); parts[ 1 ] = new RichString( s.Offset + offset, s.Length - offset, s.Style, this ); if (insertParts) { int index = myParts.IndexOf( s ); myParts.Remove( s ); myParts.Insert( index, parts[ 0 ] ); myParts.Insert( index + 1, parts[ 1 ] ); } return parts; } /// /// Gets part which contains the specified offset /// private RichString GetPartByOffset( int offset ) { int currentOffset = 0; foreach( RichString s in myParts ) { currentOffset += s.Length; if (currentOffset > offset) return s; } return null; } #endregion #region ICloneable Members public object Clone() { RichText clone = new RichText( myString, myParameters, myParts ); return clone; } public int GetCharByOffset( Point point, IntPtr hdc ) { SizeF size = GetSize( hdc ); if (point.Y < 0 || point.Y > size.Height) return -1; return GetCharByOffset( point.X, hdc ); } #endregion /// /// Gets a readonly collection of the formatting elements. /// [NotNull] public IList GetFormattedParts() { return (IList)myParts.ToArray(typeof(RichString)); } } }