///
/// 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;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using JetBrains.Omea.OpenAPI;
using JetBrains.Omea.TextIndex;
using JetBrains.UI.Interop;
namespace JetBrains.Omea.GUIControls
{
///
/// The RTF text box control.
///
public class JetRichTextBox : RichTextBox, ICommandProcessor, IContextProvider
{
#region Data
private IContextProvider _contextProvider;
private bool _showContextMenu = true;
private static Regex _rxProtocol = new Regex( "^[A-Za-z]+:" );
///
/// A set of background and foreground colors for highlighting the words in HTML text
///
protected static BiColor[] _colorsHighlight = InitColors();
public class BiColor
{
public Color ForeColor;
public Color BackColor;
public BiColor( Color colorFore, Color colorBack )
{
ForeColor = colorFore;
BackColor = colorBack;
}
}
///
/// Stores the list of search hits (derived from the WordPtr passed in for highlighting via either scheme) to navigate them and scroll to the first entry when the page loads.
/// The field contains the ready-for-use offset in the RTF content.
/// Should be Null when displaying content without the search terms and non-Null if there are search terms present.
///
protected WordPtr[] _wordsSearchHits = null;
///
/// The current search hit. This variable is used for navigating to a prev/next search hit in the document, and is updated upon the navigation.
/// -1 means that either there are no search hits, or there are ones, but we're currently positioned at none (before the first or after the last one).
///
protected int _nCurrentSearchHit = -1;
///
/// If an asynchronous highlighting procedure is in progress, persists its state between the operations. Otherwise, Null.
///
protected AsyncHighlightState _stateHilite = null;
///
/// While the highlighting is in progress, indicates it.
///
protected IStatusWriter _statuswriter = null;
///
/// Flag is True when the KeyDown event has been suppressed and the following KeyPress one should be suppressed as well.
///
protected bool _isKeyPressHandled = false;
#endregion
#region Construction
public JetRichTextBox()
{
HideSelection = false;
DetectUrls = true;
}
#endregion
#region Attributes
public IContextProvider ContextProvider
{
get { return _contextProvider; }
set { _contextProvider = value; }
}
[DefaultValue( true )]
public bool ShowContextMenu
{
get { return _showContextMenu; }
set { _showContextMenu = value; }
}
///
/// Rich text property which does not use EM_STREAMOUT and EM_STREAMIN for getting
/// and setting the text.
///
[Browsable( false )]
public string RichText
{
set
{
_wordsSearchHits = null;
SETTEXTEX ste = new SETTEXTEX();
ste.flags = 0;
ste.codepage = 0;
byte[] data = Encoding.Default.GetBytes( value );
byte[] pszData = new byte[data.Length + 1];
Array.Copy( data, pszData, data.Length );
pszData[ data.Length ] = 0;
int rc = Win32Declarations.SendMessage( Handle, EditMessage.SETTEXTEX, ref ste, pszData );
if( rc != 1 )
{
throw new Exception( "Setting RichEdit rich text failed" );
}
}
}
///
/// Plain text property which does not use EM_STREAMOUT and EM_STREAMIN for getting
/// and setting the text.
///
[Browsable( false )]
public string PlainText
{
get
{
int textLength = Win32Declarations.SendMessage( Handle, Win32Declarations.WM_GETTEXTLENGTH,
IntPtr.Zero, IntPtr.Zero );
StringBuilder result = new StringBuilder( textLength );
Win32Declarations.SendMessage( Handle, Win32Declarations.WM_GETTEXT,
(IntPtr)textLength, result );
return result.ToString();
}
}
#endregion
#region Overrides
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
_stateHilite = null; // Stop the currently-running highlighting
if( _statuswriter != null )
{
_statuswriter.ClearStatus();
_statuswriter = null;
}
_wordsSearchHits = null;
_nCurrentSearchHit = -1;
}
protected override void WndProc( ref Message m )
{
base.WndProc( ref m );
if( m.Msg == Win32Declarations.WM_CONTEXTMENU && _showContextMenu )
{
Point pnt = new Point( m.LParam.ToInt32() );
if( pnt.X == -1 && pnt.Y == -1 )
{
DisplayContextMenu( 4, 4 );
}
else
{
DisplayContextMenu( pnt.X, pnt.Y );
}
}
}
protected override void OnMouseUp( MouseEventArgs e )
{
base.OnMouseUp( e );
if( e.Button == MouseButtons.Right && _showContextMenu )
{
DisplayContextMenu( e.X, e.Y );
}
}
protected override void OnLinkClicked( LinkClickedEventArgs e )
{
string url = e.LinkText;
if( !_rxProtocol.IsMatch( url ) )
{
url = "http://" + url;
}
Core.UIManager.OpenInNewBrowserWindow( url );
}
protected override void OnKeyDown(KeyEventArgs e)
{
_isKeyPressHandled = false;
// If this is not an editor-specific key, pass it for processing to the action manager
if((!JetTextBox.IsEditorKey(e.KeyData)) && (Core.ActionManager != null))
{
if(Core.ActionManager.ExecuteKeyboardAction( GetContext(ActionContextKind.Keyboard), e.KeyData ))
e.Handled = _isKeyPressHandled = true;
}
// If not an Omea shortcut, pass it to the editbox implementation
if(! e.Handled )
base.OnKeyDown( e );
}
protected override void OnKeyPress( KeyPressEventArgs e )
{
if( _isKeyPressHandled )
e.Handled = true;
if( !e.Handled )
base.OnKeyPress( e );
}
#endregion
#region Implementation
private void DisplayContextMenu( int x, int y )
{
// Check if the control is visible (that's required to show the context menu)
if( !Visible )
{
Trace.WriteLine( "Cannot show a context menu for the invisible control.", "[JRTB]" );
return;
}
// TODO: remove the debug trace
Trace.WriteLine( "Rendering context menu for the RTF control.", "[JRTB]" );
// Show the context menu
Core.ActionManager.ShowResourceContextMenu( GetContext( ActionContextKind.ContextMenu ), this, x, y );
}
#endregion
#region Highlighting Support
#region Class AsyncHighlightState — Stores information about an async highlighting procedure in progress.
///
/// Stores information about an async highlighting procedure in progress.
///
protected class AsyncHighlightState
{
///
/// Search hits to be highlighted.
///
public WordPtr[] Words;
///
/// RTF helper struct.
///
public CHARFORMAT2 Fmt;
///
/// Enumerates whatever appropriate for the current highlighting scheme.
///
public IEnumerator Enum = null;
///
/// A function that that implements a single highlighting step, either for main or backup scheme.
/// This defines whether the highlighting will apply for main or backup scheme.
///
public StepHiliteAny StepHiliteDelegate;
///
/// A delegate type for the function that implements a single highlighting step, either for main or backup scheme.
///
public delegate bool StepHiliteAny( AsyncHighlightState state );
///
/// Backup sceme stores its hits in here.
/// A list of WordPtr objects that represent the search hits in the document; they may not correspond to the words list passed in.
/// This new list is sorted and stored for search result navigation, etc.
///
public ArrayList ActualSearchHitsCache;
///
/// The final version of the search hits list, as they were encountered in the document.
///
public WordPtr[] ActualSearchHits;
///
/// Current position in the backup highlighting scheme.
///
public int CurPos;
///
/// Maps the original word forms to the corresponding color for highlighting.
/// Holds the original search string entries that we have met, mapped to the indexes of highlighting colors.
/// Provides for highlighing tokens produced from the same search entry with the same color.
///
public Hashtable HashSources;
///
/// Stores all the target word forms in the way they should be encountered in the text, for the backup hilite to search for them, one by one.
/// Maps the word form (key) to the whole that references it (value).
///
public Hashtable HashWordForms;
///
/// Stores the time of a prev repaint.
///
public uint LastRepaintTime = 0;
///
/// Initializes the object to an indeterminate state.
///
public AsyncHighlightState( WordPtr[] words )
{
Words = words;
Fmt = new CHARFORMAT2();
Fmt.cbSize = Marshal.SizeOf( Fmt );
Fmt.dwMask = CFM.BACKCOLOR | CFM.COLOR;
HashSources = new Hashtable();
}
///
/// Chooses the color that corresponds to the given source form string, or assigns a new one if not specified yet.
/// Applies this color to the RTF structures.
///
public void PickNextColor( string source )
{
// Choose a color
BiColor color;
if( HashSources.ContainsKey( source ) ) // Source already known and has a color assigned
color = (BiColor)HashSources[ source ];
else // Assign a new color
{
color = _colorsHighlight[ HashSources.Count % _colorsHighlight.Length ];
HashSources[ source ] = color;
}
Fmt.crBackColor = ColorTranslator.ToWin32( color.BackColor );
Fmt.crTextColor = ColorTranslator.ToWin32( color.ForeColor );
}
}
#endregion
///
/// Initiates the async highlighting of the search hits.
///
/// Words to be highlighted. MUST belong to a single document section.
public void HighlightWords( WordPtr[] words )
{
#region Preconditions
if ( words == null )
return;
// Validness check
WordPtr.AssertValid( words, true );
#endregion Preconditions
_wordsSearchHits = null; // Here the real search hits will be stored; in case of main highlighting they correspond to the ones passed in
_statuswriter = Core.UIManager.GetStatusWriter( this, StatusPane.UI );
_statuswriter.ShowStatus( "Highlighting search hits in the document…" );
// Initiate the async hilite process
_stateHilite = new AsyncHighlightState( words );
if( StartHiliteMain( _stateHilite ) )
Core.UserInterfaceAP.QueueJobAt( DateTime.Now.AddMilliseconds( 500 ), "Highlight the search hits.", new MethodInvoker( StepHilite ) ); // Succeeded — queue execution
else
{ // Failed — deinitialize
_stateHilite = null;
_statuswriter.ClearStatus();
_statuswriter = null;
Trace.WriteLine( "Failed to initiate the main highlighting scheme.", "[JRTB]" );
}
}
///
/// Does the asynchronous highlighting step.
///
private void StepHilite()
{
if( _stateHilite == null )
return; // Has been shut down
uint dwStart = Win32Declarations.GetTickCount();
uint dwLimit = 222; // Allow running for this much milliseconds continuously
// Freeze the control
Win32Declarations.SendMessage( Handle, Win32Declarations.WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero );
try
{
int nIterations;
for( nIterations = 0; Win32Declarations.GetTickCount() - dwStart < dwLimit; nIterations++ ) // Work for some limited time
{
if( !_stateHilite.StepHiliteDelegate( _stateHilite ) ) // Invoke the individual highlighting step
{ // Highlighting Completed!
// Reset the status bar dials
_statuswriter.ClearStatus();
_statuswriter = null;
// Retrieve the values
_wordsSearchHits = _stateHilite.ActualSearchHits;
_nCurrentSearchHit = -1;
// Deinitialize the hilite search
_stateHilite = null;
// Jump to the next search hit
GotoNextSearchHit( true, false );
// Invalidate
Win32Declarations.SendMessage( Handle, Win32Declarations.WM_SETREDRAW, (IntPtr)1, IntPtr.Zero );
Invalidate();
// Done!
Trace.WriteLine( String.Format( "The JetRichTextBox has completed the async highlighting with {0} hits total.", (_wordsSearchHits != null ? _wordsSearchHits.Length.ToString() : "#ERROR#") ), "[JRTB]" );
return;
}
}
Trace.WriteLine( String.Format( "The JetRichTextBox async highlighting has done {0} highlightings on this step.", nIterations ), "[JRTB]" );
}
finally
{
// Unfreeze the events and repaint
Win32Declarations.SendMessage( Handle, Win32Declarations.WM_SETREDRAW, (IntPtr)1, IntPtr.Zero );
if( (_stateHilite != null) && (Win32Declarations.GetTickCount() - _stateHilite.LastRepaintTime > 2000) ) // Repaint rarely
{
Invalidate();
_stateHilite.LastRepaintTime = Win32Declarations.GetTickCount();
}
}
// Requeue the rest of execution
Application.DoEvents(); // Without this, the painting events won't occur
Core.UserInterfaceAP.QueueJob( "Highlight the search hits.", new MethodInvoker( StepHilite ) );
}
///
/// Starts applying the main highlighting scheme that uses the provided offsets for highlighting the text.
/// In case the offsets do not hit the expected words, aborts the highlighting and returns Null,
/// indicating that the backup scheme should be used.
///
/// Success flag.
protected bool StartHiliteMain( AsyncHighlightState state )
{
state.StepHiliteDelegate = new AsyncHighlightState.StepHiliteAny( StepHiliteMain );
// Assign the entries for highlighting
state.Enum = state.Words.GetEnumerator();
return true;
}
///
/// Starts applying the backup hilite scheme that applies when the main scheme fails.
/// It searches for the entries in the text and ignores the offsets given as they're assumed to be incorrect.
///
/// Success flag.
protected bool StartHiliteBackup( AsyncHighlightState state )
{
state.StepHiliteDelegate = new AsyncHighlightState.StepHiliteAny( StepHiliteBackup );
// Make a hash of the words to highlight (the particular word forms)
state.HashWordForms = new Hashtable( state.Words.Length );
foreach( WordPtr word in state.Words )
state.HashWordForms[ word.Text ] = word;
Trace.WriteLine( String.Format( "{0} unique forms were picked out of {1} original word-ptrs.", state.HashWordForms.Count, state.Words.Length ), "[JRTB]" );
// Seed the process
state.Enum = state.HashWordForms.Keys.GetEnumerator();
state.ActualSearchHitsCache = new ArrayList( state.HashWordForms.Count );
state.CurPos = -1;
// Go on
return true;
}
///
/// Applies one step of the async highlighting against the main scheme.
///
/// Whether another step should be called.
protected bool StepHiliteMain( AsyncHighlightState state )
{
if( !state.Enum.MoveNext() )
{
state.ActualSearchHits = state.Words; // As the main scheme has succeeded, the original word list is the same as the final one
return false; // Over!
}
// Get the current word
WordPtr word = (WordPtr)state.Enum.Current;
if( word.StartOffset < 0 )
return StartHiliteBackup( state ); // Fallback to the backup scheme
// Choose a color for highlighting this word
state.PickNextColor( word.Original );
// Select the supposed search hit location
Select( word.StartOffset, word.Text.Length );
// Check whether we've selected the proper thing
if( String.Compare( SelectedText, word.Text, true, CultureInfo.InvariantCulture ) != 0 )
{
Trace.WriteLine( String.Format( "Main highlighting expected to find \"{0}\" but got \"{1}\", aborting.", word.Text, SelectedText ), "[JRTB]" );
return StartHiliteBackup( state ); // Fallback to the backup scheme
}
// Apply the coloring!!
Win32Declarations.SendMessage( Handle, EditMessage.SETCHARFORMAT, SCF.SELECTION, ref state.Fmt );
return true; // Call more
}
///
/// Applies one step of the async highlighting against the backup scheme.
///
/// Whether another step should be called.
protected bool StepHiliteBackup( AsyncHighlightState state )
{
if( state.CurPos < 0 ) // Should we pick a new word form for searching it?
{
if( !state.Enum.MoveNext() )
{ // Completed!!
// Sort the search hits in order of appearance and supply to the storage
state.ActualSearchHitsCache.Sort( new WordPtrOffsetComparer() );
state.ActualSearchHits = (WordPtr[])state.ActualSearchHitsCache.ToArray( typeof(WordPtr) ); // Take the search hits
return false; // Finish it
}
state.CurPos = 0; // Start looking for it from the beginning
}
string sOriginal = (string)state.Enum.Current;
WordPtr wordSearchHit = (WordPtr)state.HashWordForms[ sOriginal ];
// Choose a color for highlighting the hits of this text
state.PickNextColor( wordSearchHit.Original );
// Look for the next entry, starting from the very place we left it the prev time
int nOldPos = state.CurPos;
state.CurPos = Find( wordSearchHit.Text, state.CurPos, RichTextBoxFinds.NoHighlight | RichTextBoxFinds.WholeWord );
if( state.CurPos < 0 ) // If not found, will be negative
return true; // Switch to looking for the next entry, or complete the process if there are no more
if(state.CurPos <= nOldPos) // Sometimes the Find function will return a result BEFORE the search start :)
{ // Switch to looking for the next entry, or complete the process if there are no more
state.CurPos = -1;
return true;
}
// Add the search hit data
WordPtr hit = wordSearchHit; // Make a copy of the hit
hit.StartOffset = state.CurPos; // The actual starting offset
state.ActualSearchHitsCache.Add( hit );
// Select the supposed search hit location
Select( state.CurPos, wordSearchHit.Text.Length );
state.CurPos += wordSearchHit.Text.Length; // Skip the already-found entry (otherwise we'll keep finding it again and again, eternally)
// Apply the coloring!!
Win32Declarations.SendMessage( Handle, EditMessage.SETCHARFORMAT, SCF.SELECTION, ref state.Fmt );
return true; // Try looking for the next entry
}
#region Hilite — Navigation
///
/// Determines whether the GotoNextSearchHit function can perform its action at this time.
///
public bool CanGotoNextSearchHit( bool bForward )
{
if( _wordsSearchHits == null ) // Search has not been performed
return false;
// If the search is currently positioned "beyond", allow if there are any search hits
if( _nCurrentSearchHit == -1 )
return _wordsSearchHits.Length != 0;
// Check for each of the directions
return ((bForward) && (_nCurrentSearchHit < _wordsSearchHits.Length - 1)) || ((!bForward) && (_nCurrentSearchHit > 0));
}
///
/// If there are search hits in the document, navigates to either previous or next one, depending on the parameter value.
/// Does not loop around the end. Never throws an exception.
///
/// If True, highlights the search hit with selection; otherwise, just scrolls to it.
private void GotoNextSearchHit( bool bForward, bool hilite )
{
// Check if allowed
if( !CanGotoNextSearchHit( bForward ) )
return;
// Position at a new hit
if( _nCurrentSearchHit == -1 ) // Beyond?
_nCurrentSearchHit = bForward ? 0 : _wordsSearchHits.Length - 1; // Position at the end
else // Positioned at some of the hits already
_nCurrentSearchHit += (bForward ? 1 : 0) * 2 - 1; // Move in the direction appropriate
// Goto
Select( _wordsSearchHits[ _nCurrentSearchHit ].StartOffset, (hilite ? _wordsSearchHits[ _nCurrentSearchHit ].Text.Length : 0) );
ScrollToCaret();
}
#endregion
#region Class WordPtrOffsetComparer — Compares the structures by their start-offsets.
///
/// Compares the structures by their start-offsets.
///
internal class WordPtrOffsetComparer : IComparer
{
public int Compare( object x, object y )
{
return ((WordPtr)x).StartOffset.CompareTo( ((WordPtr)y).StartOffset ); // Compare by the starting offsets, using the int's comparer
}
}
#endregion
///
/// Initializes the colors that are used for highlighting the search keywords in the page text
///
protected static BiColor[] InitColors()
{
BiColor[] colorsHighlight = new BiColor[8];
colorsHighlight[ 0 ] = new BiColor( Color.Black, Color.FromArgb( 0xF5, 0xC6, 0x8E ) );
colorsHighlight[ 1 ] = new BiColor( Color.Black, Color.FromArgb( 0xAA, 0xA6, 0xDD ) );
colorsHighlight[ 2 ] = new BiColor( Color.Black, Color.FromArgb( 0xE0, 0xA2, 0xE1 ) );
colorsHighlight[ 3 ] = new BiColor( Color.Black, Color.FromArgb( 0x94, 0xDC, 0xEE ) );
colorsHighlight[ 4 ] = new BiColor( Color.Black, Color.FromArgb( 0xF4, 0xFF, 0x84 ) );
colorsHighlight[ 5 ] = new BiColor( Color.Black, Color.FromArgb( 0xB4, 0xF5, 0x8E ) );
colorsHighlight[ 6 ] = new BiColor( Color.Black, Color.FromArgb( 0xF5, 0x95, 0x8E ) );
colorsHighlight[ 7 ] = new BiColor( Color.Black, Color.FromArgb( 0x8E, 0xF5, 0xD1 ) );
return colorsHighlight;
}
#endregion
#region ICommandProcessor Interface
public void ExecuteCommand( string command )
{
switch( command )
{
case DisplayPaneCommands.Copy:
Copy();
break;
case DisplayPaneCommands.Cut:
Cut();
break;
case DisplayPaneCommands.Paste:
Paste();
break;
case DisplayPaneCommands.SelectAll:
SelectAll();
break;
case DisplayPaneCommands.PrevSearchResult:
GotoNextSearchHit( false, true );
break;
case DisplayPaneCommands.NextSearchResult:
GotoNextSearchHit( true, true );
break;
case "Undo":
Undo();
break;
case "Redo":
Redo();
break;
}
}
public bool CanExecuteCommand( string command )
{
switch( command )
{
case DisplayPaneCommands.Copy:
return (SelectionLength != 0);
case DisplayPaneCommands.Cut:
return (SelectionLength != 0);
case DisplayPaneCommands.Paste:
return true;
case DisplayPaneCommands.SelectAll:
return true;
case DisplayPaneCommands.PrevSearchResult:
return CanGotoNextSearchHit( false );
case DisplayPaneCommands.NextSearchResult:
return CanGotoNextSearchHit( true );
case "Undo":
return CanUndo;
case "Redo":
return CanRedo;
default:
return false;
}
}
#endregion
#region IContextProvider Members
public IActionContext GetContext( ActionContextKind kind )
{
ActionContext context;
if( _contextProvider != null )
context = (ActionContext)_contextProvider.GetContext( kind );
else
{
context = new ActionContext( kind, this, null );
context.SetCommandProcessor( this );
}
context.SetSelectedText( SelectedRtf, SelectedText, TextFormat.Rtf );
return context;
}
#endregion
}
}