///
/// 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));
}
}
}