diff options
| author | Jørgen P. Tjernø <[email protected]> | 2013-12-02 19:31:46 -0800 |
|---|---|---|
| committer | Jørgen P. Tjernø <[email protected]> | 2013-12-02 19:46:31 -0800 |
| commit | f56bb35301836e56582a575a75864392a0177875 (patch) | |
| tree | de61ddd39de3e7df52759711950b4c288592f0dc /mp/src/vgui2/vgui_controls/RichText.cpp | |
| parent | Mark some more files as text. (diff) | |
| download | source-sdk-2013-f56bb35301836e56582a575a75864392a0177875.tar.xz source-sdk-2013-f56bb35301836e56582a575a75864392a0177875.zip | |
Fix line endings. WHAMMY.
Diffstat (limited to 'mp/src/vgui2/vgui_controls/RichText.cpp')
| -rw-r--r-- | mp/src/vgui2/vgui_controls/RichText.cpp | 5488 |
1 files changed, 2744 insertions, 2744 deletions
diff --git a/mp/src/vgui2/vgui_controls/RichText.cpp b/mp/src/vgui2/vgui_controls/RichText.cpp index c4c76682..8d28c4ee 100644 --- a/mp/src/vgui2/vgui_controls/RichText.cpp +++ b/mp/src/vgui2/vgui_controls/RichText.cpp @@ -1,2744 +1,2744 @@ -//========= Copyright Valve Corporation, All rights reserved. ============//
-//
-// Purpose:
-//
-// $NoKeywords: $
-//=============================================================================//
-
-#include "vgui_controls/pch_vgui_controls.h"
-#include "vgui/ILocalize.h"
-
-// memdbgon must be the last include file in a .cpp file
-#include "tier0/memdbgon.h"
-
-enum
-{
- MAX_BUFFER_SIZE = 999999, // maximum size of text buffer
- DRAW_OFFSET_X = 3,
- DRAW_OFFSET_Y = 1,
-};
-
-using namespace vgui;
-
-#ifndef max
-#define max(a,b) (((a) > (b)) ? (a) : (b))
-#endif
-
-namespace vgui
-{
-
-//#define DRAW_CLICK_PANELS
-
-//-----------------------------------------------------------------------------
-// Purpose: Panel used for clickable URL's
-//-----------------------------------------------------------------------------
-class ClickPanel : public Panel
-{
- DECLARE_CLASS_SIMPLE( ClickPanel, Panel );
-
-public:
- ClickPanel(Panel *parent)
- {
- _viewIndex = 0;
- _textIndex = 0;
- SetParent(parent);
- AddActionSignalTarget(parent);
-
- SetCursor(dc_hand);
-
- SetPaintBackgroundEnabled(false);
- SetPaintEnabled(false);
-// SetPaintAppearanceEnabled(false);
-
-#if defined( DRAW_CLICK_PANELS )
- SetPaintEnabled(true);
-#endif
- }
-
- void SetTextIndex( int linkStartIndex, int viewStartIndex )
- {
- _textIndex = linkStartIndex;
- _viewIndex = viewStartIndex;
- }
-
-#if defined( DRAW_CLICK_PANELS )
- virtual void Paint()
- {
- surface()->DrawSetColor( Color( 255, 0, 0, 255 ) );
- surface()->DrawOutlinedRect( 0, 0, GetWide(), GetTall() );
- }
-#endif
-
- int GetTextIndex()
- {
- return _textIndex;
- }
-
- int GetViewTextIndex()
- {
- return _viewIndex;
- }
-
- void OnMousePressed(MouseCode code)
- {
- if (code == MOUSE_LEFT)
- {
- PostActionSignal(new KeyValues("ClickPanel", "index", _textIndex));
- }
- else
- {
- GetParent()->OnMousePressed( code );
- }
- }
-
-private:
- int _textIndex;
- int _viewIndex;
-};
-
-
-//-----------------------------------------------------------------------------
-// Purpose: Panel used only to draw the interior border region
-//-----------------------------------------------------------------------------
-class RichTextInterior : public Panel
-{
- DECLARE_CLASS_SIMPLE( RichTextInterior, Panel );
-
-public:
- RichTextInterior( RichText *pParent, const char *pchName ) : BaseClass( pParent, pchName )
- {
- SetKeyBoardInputEnabled( false );
- SetMouseInputEnabled( false );
- SetPaintBackgroundEnabled( false );
- SetPaintEnabled( false );
- m_pRichText = pParent;
- }
-
-/* virtual IAppearance *GetAppearance()
- {
- if ( m_pRichText->IsScrollbarVisible() )
- return m_pAppearanceScrollbar;
-
- return BaseClass::GetAppearance();
- }*/
-
- virtual void ApplySchemeSettings( IScheme *pScheme )
- {
- BaseClass::ApplySchemeSettings( pScheme );
-// m_pAppearanceScrollbar = FindSchemeAppearance( pScheme, "scrollbar_visible" );
- }
-
-private:
- RichText *m_pRichText;
-// IAppearance *m_pAppearanceScrollbar;
-};
-
-}; // namespace vgui
-
-DECLARE_BUILD_FACTORY( RichText );
-
-//-----------------------------------------------------------------------------
-// Purpose: Constructor
-//-----------------------------------------------------------------------------
-RichText::RichText(Panel *parent, const char *panelName) : BaseClass(parent, panelName)
-{
- m_bAllTextAlphaIsZero = false;
- _font = INVALID_FONT;
- m_hFontUnderline = INVALID_FONT;
-
- m_bRecalcLineBreaks = true;
- m_pszInitialText = NULL;
- _cursorPos = 0;
- _mouseSelection = false;
- _mouseDragSelection = false;
- _vertScrollBar = new ScrollBar(this, "ScrollBar", true);
- _vertScrollBar->AddActionSignalTarget(this);
- _recalcSavedRenderState = true;
- _maxCharCount = (64 * 1024);
- AddActionSignalTarget(this);
- m_pInterior = new RichTextInterior( this, NULL );
-
- //a -1 for _select[0] means that the selection is empty
- _select[0] = -1;
- _select[1] = -1;
- m_pEditMenu = NULL;
-
- SetCursor(dc_ibeam);
-
- //position the cursor so it is at the end of the text
- GotoTextEnd();
-
- // set default foreground color to black
- _defaultTextColor = Color(0, 0, 0, 0);
-
- // initialize the line break array
- InvalidateLineBreakStream();
-
- if ( IsProportional() )
- {
- int width, height;
- int sw,sh;
- surface()->GetProportionalBase( width, height );
- surface()->GetScreenSize(sw, sh);
-
- _drawOffsetX = static_cast<int>( static_cast<float>( DRAW_OFFSET_X )*( static_cast<float>( sw )/ static_cast<float>( width )));
- _drawOffsetY = static_cast<int>( static_cast<float>( DRAW_OFFSET_Y )*( static_cast<float>( sw )/ static_cast<float>( width )));
- }
- else
- {
- _drawOffsetX = DRAW_OFFSET_X;
- _drawOffsetY = DRAW_OFFSET_Y;
- }
-
- // add a basic format string
- TFormatStream stream;
- stream.color = _defaultTextColor;
- stream.fade.flFadeStartTime = 0.0f;
- stream.fade.flFadeLength = -1.0f;
- stream.pixelsIndent = 0;
- stream.textStreamIndex = 0;
- stream.textClickable = false;
- m_FormatStream.AddToTail(stream);
-
- m_bResetFades = false;
- m_bInteractive = true;
- m_bUnusedScrollbarInvis = false;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Destructor
-//-----------------------------------------------------------------------------
-RichText::~RichText()
-{
- delete [] m_pszInitialText;
- delete m_pEditMenu;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::SetDrawOffsets( int ofsx, int ofsy )
-{
- _drawOffsetX = ofsx;
- _drawOffsetY = ofsy;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: sets it as drawing text only - used for embedded RichText control into other text drawing situations
-//-----------------------------------------------------------------------------
-void RichText::SetDrawTextOnly()
-{
- SetDrawOffsets( 0, 0 );
- SetPaintBackgroundEnabled( false );
-// SetPaintAppearanceEnabled( false );
- SetPostChildPaintEnabled( false );
- m_pInterior->SetVisible( false );
- SetVerticalScrollbar( false );
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: configures colors
-//-----------------------------------------------------------------------------
-void RichText::ApplySchemeSettings(IScheme *pScheme)
-{
- BaseClass::ApplySchemeSettings(pScheme);
-
- _font = pScheme->GetFont("Default", IsProportional() );
- m_hFontUnderline = pScheme->GetFont("DefaultUnderline", IsProportional() );
-
- SetFgColor(GetSchemeColor("RichText.TextColor", pScheme));
- SetBgColor(GetSchemeColor("RichText.BgColor", pScheme));
-
- _selectionTextColor = GetSchemeColor("RichText.SelectedTextColor", GetFgColor(), pScheme);
- _selectionColor = GetSchemeColor("RichText.SelectedBgColor", pScheme);
-
- if ( Q_strlen( pScheme->GetResourceString( "RichText.InsetX" ) ) )
- {
- SetDrawOffsets( atoi( pScheme->GetResourceString( "RichText.InsetX" ) ), atoi( pScheme->GetResourceString( "RichText.InsetY" ) ) );
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: if the default format color isn't set then set it
-//-----------------------------------------------------------------------------
-void RichText::SetFgColor( Color color )
-{
- // Replace default format color if
- // the stream is empty and the color is the default ( or the previous FgColor )
- if ( m_FormatStream.Size() == 1 &&
- ( m_FormatStream[0].color == _defaultTextColor || m_FormatStream[0].color == GetFgColor() ) )
- {
- m_FormatStream[0].color = color;
- }
-
- BaseClass::SetFgColor( color );
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Sends a message if the data has changed
-// Turns off any selected text in the window if we are not using the edit menu
-//-----------------------------------------------------------------------------
-void RichText::OnKillFocus()
-{
- // check if we clicked the right mouse button or if it is down
- bool mouseRightClicked = input()->WasMousePressed(MOUSE_RIGHT);
- bool mouseRightUp = input()->WasMouseReleased(MOUSE_RIGHT);
- bool mouseRightDown = input()->IsMouseDown(MOUSE_RIGHT);
-
- if (mouseRightClicked || mouseRightDown || mouseRightUp )
- {
- // get the start and ends of the selection area
- int start, end;
- if (GetSelectedRange(start, end)) // we have selected text
- {
- // see if we clicked in the selection area
- int startX, startY;
- CursorToPixelSpace(start, startX, startY);
- int endX, endY;
- CursorToPixelSpace(end, endX, endY);
- int cursorX, cursorY;
- input()->GetCursorPos(cursorX, cursorY);
- ScreenToLocal(cursorX, cursorY);
-
- // check the area vertically
- // we need to handle the horizontal edge cases eventually
- int fontTall = GetLineHeight();
- endY = endY + fontTall;
- if ((startY < cursorY) && (endY > cursorY))
- {
- // if we clicked in the selection area, leave the text highlighted
- return;
- }
- }
- }
-
- // clear any selection
- SelectNone();
-
- // chain
- BaseClass::OnKillFocus();
-}
-
-
-//-----------------------------------------------------------------------------
-// Purpose: Wipe line breaks after the size of a panel has been changed
-//-----------------------------------------------------------------------------
-void RichText::OnSizeChanged( int wide, int tall )
-{
- BaseClass::OnSizeChanged( wide, tall );
-
- // blow away the line breaks list
- _invalidateVerticalScrollbarSlider = true;
- InvalidateLineBreakStream();
- InvalidateLayout();
-
- if ( _vertScrollBar->IsVisible() )
- {
- _vertScrollBar->MakeReadyForUse();
- m_pInterior->SetBounds( 0, 0, wide - _vertScrollBar->GetWide(), tall );
- }
- else
- {
- m_pInterior->SetBounds( 0, 0, wide, tall );
- }
-}
-
-
-const wchar_t *RichText::ResolveLocalizedTextAndVariables( char const *pchLookup, wchar_t *outbuf, size_t outbufsizeinbytes )
-{
- if ( pchLookup[ 0 ] == '#' )
- {
- // try lookup in localization tables
- StringIndex_t index = g_pVGuiLocalize->FindIndex( pchLookup + 1 );
- if ( index == INVALID_LOCALIZE_STRING_INDEX )
- {
-/* // if it's not found, maybe it's a special expanded variable - look for an expansion
- char rgchT[MAX_PATH];
-
- // get the variables
- KeyValues *variables = GetDialogVariables_R();
- if ( variables )
- {
- // see if any are any special vars to put in
- for ( KeyValues *pkv = variables->GetFirstSubKey(); pkv != NULL; pkv = pkv->GetNextKey() )
- {
- if ( !Q_strncmp( pkv->GetName(), "$", 1 ) )
- {
- // make a new lookup, with this key appended
- Q_snprintf( rgchT, sizeof( rgchT ), "%s%s=%s", pchLookup, pkv->GetName(), pkv->GetString() );
- index = localize()->FindIndex( rgchT );
- break;
- }
- }
- }
- */
- }
-
- // see if we have a valid string
- if ( index != INVALID_LOCALIZE_STRING_INDEX )
- {
- wchar_t *format = g_pVGuiLocalize->GetValueByIndex( index );
- Assert( format );
- if ( format )
- {
- /*// Try and substitute variables if any
- KeyValues *variables = GetDialogVariables_R();
- if ( variables )
- {
- localize()->ConstructString( outbuf, outbufsizeinbytes, index, variables );
- return outbuf;
- }*/
- }
- V_wcsncpy( outbuf, format, outbufsizeinbytes );
- return outbuf;
- }
- }
-
- Q_UTF8ToUnicode( pchLookup, outbuf, outbufsizeinbytes );
- return outbuf;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Set the text array
-// Using this function will cause all lineBreaks to be discarded.
-// This is because this fxn replaces the contents of the text buffer.
-// For modifying large buffers use insert functions.
-//-----------------------------------------------------------------------------
-void RichText::SetText(const char *text)
-{
- if (!text)
- {
- text = "";
- }
-
- wchar_t unicode[1024];
-
- if (text[0] == '#')
- {
- ResolveLocalizedTextAndVariables( text, unicode, sizeof( unicode ) );
- SetText( unicode );
- return;
- }
-
- // convert to unicode
- Q_UTF8ToUnicode(text, unicode, sizeof(unicode));
- SetText(unicode);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::SetText(const wchar_t *text)
-{
- // reset the formatting stream
- m_FormatStream.RemoveAll();
- TFormatStream stream;
- stream.color = GetFgColor();
- stream.fade.flFadeLength = -1.0f;
- stream.fade.flFadeStartTime = 0.0f;
- stream.pixelsIndent = 0;
- stream.textStreamIndex = 0;
- stream.textClickable = false;
- m_FormatStream.AddToTail(stream);
-
- // set the new text stream
- m_TextStream.RemoveAll();
- if ( text && *text )
- {
- int textLen = wcslen(text) + 1;
- m_TextStream.EnsureCapacity(textLen);
- for(int i = 0; i < textLen; i++)
- {
- m_TextStream.AddToTail(text[i]);
- }
- }
- GotoTextStart();
- SelectNone();
-
- // blow away the line breaks list
- InvalidateLineBreakStream();
- InvalidateLayout();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Given cursor's position in the text buffer, convert it to
-// the local window's x and y pixel coordinates
-// Input: cursorPos: cursor index
-// Output: cx, cy, the corresponding coords in the local window
-//-----------------------------------------------------------------------------
-void RichText::CursorToPixelSpace(int cursorPos, int &cx, int &cy)
-{
- int yStart = _drawOffsetY;
- int x = _drawOffsetX, y = yStart;
- _pixelsIndent = 0;
- int lineBreakIndexIndex = 0;
-
- for (int i = GetStartDrawIndex(lineBreakIndexIndex); i < m_TextStream.Count(); i++)
- {
- wchar_t ch = m_TextStream[i];
-
- // if we've found the position, break
- if (cursorPos == i)
- {
- // if we've passed a line break go to that
- if (m_LineBreaks[lineBreakIndexIndex] == i)
- {
- // add another line
- AddAnotherLine(x, y);
- lineBreakIndexIndex++;
- }
- break;
- }
-
- // if we've passed a line break go to that
- if (m_LineBreaks[lineBreakIndexIndex] == i)
- {
- // add another line
- AddAnotherLine(x, y);
- lineBreakIndexIndex++;
- }
-
- // add to the current position
- x += surface()->GetCharacterWidth(_font, ch);
- }
-
- cx = x;
- cy = y;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Converts local pixel coordinates to an index in the text buffer
-//-----------------------------------------------------------------------------
-int RichText::PixelToCursorSpace(int cx, int cy)
-{
- int fontTall = GetLineHeight();
-
- // where to start reading
- int yStart = _drawOffsetY;
- int x = _drawOffsetX, y = yStart;
- _pixelsIndent = 0;
- int lineBreakIndexIndex = 0;
-
- int startIndex = GetStartDrawIndex(lineBreakIndexIndex);
- if (_recalcSavedRenderState)
- {
- RecalculateDefaultState(startIndex);
- }
-
- _pixelsIndent = m_CachedRenderState.pixelsIndent;
- _currentTextClickable = m_CachedRenderState.textClickable;
- TRenderState renderState = m_CachedRenderState;
-
- bool onRightLine = false;
- int i;
- for (i = startIndex; i < m_TextStream.Count(); i++)
- {
- wchar_t ch = m_TextStream[i];
-
- renderState.x = x;
- if ( UpdateRenderState( i, renderState ) )
- {
- x = renderState.x;
- }
-
- // if we are on the right line but off the end of if put the cursor at the end of the line
- if (m_LineBreaks[lineBreakIndexIndex] == i)
- {
- // add another line
- AddAnotherLine(x, y);
- lineBreakIndexIndex++;
-
- if (onRightLine)
- break;
- }
-
- // check to see if we're on the right line
- if (cy < yStart)
- {
- // cursor is above panel
- onRightLine = true;
- }
- else if (cy >= y && (cy < (y + fontTall + _drawOffsetY)))
- {
- onRightLine = true;
- }
-
- int wide = surface()->GetCharacterWidth(_font, ch);
-
- // if we've found the position, break
- if (onRightLine)
- {
- if (cx > GetWide()) // off right side of window
- {
- }
- else if (cx < (_drawOffsetX + renderState.pixelsIndent) || cy < yStart) // off left side of window
- {
- // Msg( "PixelToCursorSpace() off left size, returning %d '%c'\n", i, m_TextStream[i] );
- return i; // move cursor one to left
- }
-
- if (cx >= x && cx < (x + wide))
- {
- // check which side of the letter they're on
- if (cx < (x + (wide * 0.5))) // left side
- {
- // Msg( "PixelToCursorSpace() on the left size, returning %d '%c'\n", i, m_TextStream[i] );
- return i;
- }
- else // right side
- {
- // Msg( "PixelToCursorSpace() on the right size, returning %d '%c'\n", i + 1, m_TextStream[i + 1] );
- return i + 1;
- }
- }
- }
- x += wide;
- }
-
- // Msg( "PixelToCursorSpace() never hit, returning %d\n", i );
- return i;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Draws a string of characters in the panel
-// Input: iFirst - Index of the first character to draw
-// iLast - Index of the last character to draw
-// renderState - Render state to use
-// font- font to use
-// Output: returns the width of the character drawn
-//-----------------------------------------------------------------------------
-int RichText::DrawString(int iFirst, int iLast, TRenderState &renderState, HFont font)
-{
-// VPROF( "RichText::DrawString" );
-
- // Calculate the render size
- int fontTall = surface()->GetFontTall(font);
- // BUGBUG John: This won't exactly match the rendered size
- int charWide = 0;
- for ( int i = iFirst; i <= iLast; i++ )
- {
- wchar_t ch = m_TextStream[i];
-#if USE_GETKERNEDCHARWIDTH
- wchar_t chBefore = 0;
- wchar_t chAfter = 0;
- if ( i > 0 )
- chBefore = m_TextStream[i-1];
- if ( i < iLast )
- chAfter = m_TextStream[i+1];
- float flWide = 0.0f, flabcA = 0.0f;
- surface()->GetKernedCharWidth(font, ch, chBefore, chAfter, flWide, flabcA);
- if ( ch == L' ' )
- flWide = ceil( flWide );
- charWide += floor( flWide + 0.6 );
-#else
- charWide += surface()->GetCharacterWidth(font, ch);
-#endif
- }
-
- // draw selection, if any
- int selection0 = -1, selection1 = -1;
- GetSelectedRange(selection0, selection1);
-
- if (iFirst >= selection0 && iFirst < selection1)
- {
- // draw background selection color
- surface()->DrawSetColor(_selectionColor);
- surface()->DrawFilledRect(renderState.x, renderState.y, renderState.x + charWide, renderState.y + 1 + fontTall);
-
- // reset text color
- surface()->DrawSetTextColor(_selectionTextColor);
- m_bAllTextAlphaIsZero = false;
- }
- else
- {
- surface()->DrawSetTextColor(renderState.textColor);
- }
-
- if ( renderState.textColor.a() != 0 )
- {
- m_bAllTextAlphaIsZero = false;
- surface()->DrawSetTextPos(renderState.x, renderState.y);
- surface()->DrawPrintText(&m_TextStream[iFirst], iLast - iFirst + 1);
- }
-
- return charWide;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Finish drawing url
-//-----------------------------------------------------------------------------
-void RichText::FinishingURL(int x, int y)
-{
- // finishing URL
- if ( _clickableTextPanels.IsValidIndex( _clickableTextIndex ) )
- {
- ClickPanel *clickPanel = _clickableTextPanels[ _clickableTextIndex ];
- int px, py;
- clickPanel->GetPos(px, py);
- int fontTall = GetLineHeight();
- clickPanel->SetSize( MAX( x - px, 6 ), y - py + fontTall );
- clickPanel->SetVisible(true);
-
- // if we haven't actually advanced any, step back and ignore this one
- // this is probably a data input problem though, need to find root cause
- if ( x - px <= 0 )
- {
- --_clickableTextIndex;
- clickPanel->SetVisible(false);
- }
- }
-}
-
-void RichText::CalculateFade( TRenderState &renderState )
-{
- if ( m_FormatStream.IsValidIndex( renderState.formatStreamIndex ) )
- {
- if ( m_bResetFades == false )
- {
- if ( m_FormatStream[renderState.formatStreamIndex].fade.flFadeLength != -1.0f )
- {
- float frac = ( m_FormatStream[renderState.formatStreamIndex].fade.flFadeStartTime - system()->GetCurrentTime() ) / m_FormatStream[renderState.formatStreamIndex].fade.flFadeLength;
-
- int alpha = frac * m_FormatStream[renderState.formatStreamIndex].fade.iOriginalAlpha;
- alpha = clamp( alpha, 0, m_FormatStream[renderState.formatStreamIndex].fade.iOriginalAlpha );
-
- renderState.textColor.SetColor( renderState.textColor.r(), renderState.textColor.g(), renderState.textColor.b(), alpha );
- }
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Draws the text in the panel
-//-----------------------------------------------------------------------------
-void RichText::Paint()
-{
- // Assume the worst
- m_bAllTextAlphaIsZero = true;
-
- HFont hFontCurrent = _font;
-
- // hide all the clickable panels until we know where they are to reside
- for (int j = 0; j < _clickableTextPanels.Count(); j++)
- {
- _clickableTextPanels[j]->SetVisible(false);
- }
-
- if ( !HasText() )
- return;
-
- int wide, tall;
- GetSize( wide, tall );
-
- int lineBreakIndexIndex = 0;
- int startIndex = GetStartDrawIndex(lineBreakIndexIndex);
- _currentTextClickable = false;
-
- _clickableTextIndex = GetClickableTextIndexStart(startIndex);
-
- // recalculate and cache the render state at the render start
- if (_recalcSavedRenderState)
- {
- RecalculateDefaultState(startIndex);
- }
- // copy off the cached render state
- TRenderState renderState = m_CachedRenderState;
-
- _pixelsIndent = m_CachedRenderState.pixelsIndent;
- _currentTextClickable = m_CachedRenderState.textClickable;
-
- renderState.textClickable = _currentTextClickable;
- renderState.textColor = m_FormatStream[renderState.formatStreamIndex].color;
- CalculateFade( renderState );
-
- renderState.formatStreamIndex++;
-
- if ( _currentTextClickable )
- {
- _clickableTextIndex = startIndex;
- }
-
- // where to start drawing
- renderState.x = _drawOffsetX + _pixelsIndent;
- renderState.y = _drawOffsetY;
-
- // draw the text
- int selection0 = -1, selection1 = -1;
- GetSelectedRange(selection0, selection1);
-
- surface()->DrawSetTextFont( hFontCurrent );
-
- for (int i = startIndex; i < m_TextStream.Count() && renderState.y < tall; )
- {
- // 1.
- // Update our current render state based on the formatting and color streams,
- // this has to happen if it's our very first iteration, or if we are actually changing
- // state.
- int nXBeforeStateChange = renderState.x;
- if ( UpdateRenderState(i, renderState) || i == startIndex )
- {
- // check for url state change
- if (renderState.textClickable != _currentTextClickable)
- {
- if (renderState.textClickable)
- {
- // entering new URL
- _clickableTextIndex++;
- hFontCurrent = m_hFontUnderline;
- surface()->DrawSetTextFont( hFontCurrent );
-
- // set up the panel
- ClickPanel *clickPanel = _clickableTextPanels.IsValidIndex( _clickableTextIndex ) ? _clickableTextPanels[_clickableTextIndex] : NULL;
-
- if (clickPanel)
- {
- clickPanel->SetPos(renderState.x, renderState.y);
- }
- }
- else
- {
- FinishingURL(nXBeforeStateChange, renderState.y);
- hFontCurrent = _font;
- surface()->DrawSetTextFont( hFontCurrent );
- }
- _currentTextClickable = renderState.textClickable;
- }
- }
-
- // 2.
- // if we've passed a line break go to that
- if ( m_LineBreaks.IsValidIndex( lineBreakIndexIndex ) && m_LineBreaks[lineBreakIndexIndex] <= i )
- {
- if (_currentTextClickable)
- {
- FinishingURL(renderState.x, renderState.y);
- }
-
- // add another line
- AddAnotherLine(renderState.x, renderState.y);
- lineBreakIndexIndex++;
-
- // Skip white space unless the previous line ended from the hard carriage return
- if ( i && ( m_TextStream[i-1] != '\n' ) && ( m_TextStream[i-1] != '\r') )
- {
- while ( m_TextStream[i] == L' ' )
- {
- if ( i+1 < m_TextStream.Count() )
- ++i;
- else
- break;
- }
- }
-
- if (renderState.textClickable)
- {
- // move to the next URL
- _clickableTextIndex++;
- ClickPanel *clickPanel = _clickableTextPanels.IsValidIndex( _clickableTextIndex ) ? _clickableTextPanels[_clickableTextIndex] : NULL;
- if (clickPanel)
- {
- clickPanel->SetPos(renderState.x, renderState.y);
- }
- }
- }
-
- // 3.
- // Calculate the range of text to draw all at once
- int iLim = m_TextStream.Count();
-
- // Stop at the next format change
- if ( m_FormatStream.IsValidIndex(renderState.formatStreamIndex) &&
- m_FormatStream[renderState.formatStreamIndex].textStreamIndex < iLim &&
- m_FormatStream[renderState.formatStreamIndex].textStreamIndex >= i &&
- m_FormatStream[renderState.formatStreamIndex].textStreamIndex )
- {
- iLim = m_FormatStream[renderState.formatStreamIndex].textStreamIndex;
- }
-
- // Stop at the next line break
- if ( m_LineBreaks.IsValidIndex( lineBreakIndexIndex ) && m_LineBreaks[lineBreakIndexIndex] < iLim )
- iLim = m_LineBreaks[lineBreakIndexIndex];
-
- // Handle non-drawing characters specially
- for ( int iT = i; iT < iLim; iT++ )
- {
- if ( iswcntrl(m_TextStream[iT]) )
- {
- iLim = iT;
- break;
- }
- }
-
- // 4.
- // Draw the current text range
- if ( iLim <= i )
- {
- if ( m_TextStream[i] == '\t' )
- {
- int dxTabWidth = 8 * surface()->GetCharacterWidth(hFontCurrent, ' ');
- dxTabWidth = MAX( 1, dxTabWidth );
-
- renderState.x = ( dxTabWidth * ( 1 + ( renderState.x / dxTabWidth ) ) );
- }
- i++;
- }
- else
- {
- renderState.x += DrawString(i, iLim - 1, renderState, hFontCurrent );
- i = iLim;
- }
- }
-
- if (renderState.textClickable)
- {
- FinishingURL(renderState.x, renderState.y);
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-int RichText::GetClickableTextIndexStart(int startIndex)
-{
- // cycle to the right url panel for what is visible after the startIndex.
- for (int i = 0; i < _clickableTextPanels.Count(); i++)
- {
- if (_clickableTextPanels[i]->GetViewTextIndex() >= startIndex)
- {
- return i - 1;
- }
- }
- return -1;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Recalcultes the formatting state from the specified index
-//-----------------------------------------------------------------------------
-void RichText::RecalculateDefaultState(int startIndex)
-{
- if (!HasText() )
- return;
-
- Assert(startIndex < m_TextStream.Count());
-
- m_CachedRenderState.textColor = GetFgColor();
- _pixelsIndent = 0;
- _currentTextClickable = false;
- _clickableTextIndex = GetClickableTextIndexStart(startIndex);
-
- // find where in the formatting stream we need to be
- GenerateRenderStateForTextStreamIndex(startIndex, m_CachedRenderState);
- _recalcSavedRenderState = false;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: updates a render state based on the formatting and color streams
-// Output: true if we changed the render state
-//-----------------------------------------------------------------------------
-bool RichText::UpdateRenderState(int textStreamPos, TRenderState &renderState)
-{
- // check the color stream
- if (m_FormatStream.IsValidIndex(renderState.formatStreamIndex) &&
- m_FormatStream[renderState.formatStreamIndex].textStreamIndex == textStreamPos)
- {
- // set the current formatting
- renderState.textColor = m_FormatStream[renderState.formatStreamIndex].color;
- renderState.textClickable = m_FormatStream[renderState.formatStreamIndex].textClickable;
-
- CalculateFade( renderState );
-
- int indentChange = m_FormatStream[renderState.formatStreamIndex].pixelsIndent - renderState.pixelsIndent;
- renderState.pixelsIndent = m_FormatStream[renderState.formatStreamIndex].pixelsIndent;
-
- if (indentChange)
- {
- renderState.x = renderState.pixelsIndent + _drawOffsetX;
- }
-
- //!! for supporting old functionality, store off state in globals
- _pixelsIndent = renderState.pixelsIndent;
-
- // move to the next position in the color stream
- renderState.formatStreamIndex++;
- return true;
- }
-
- return false;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Returns the index in the format stream for the specified text stream index
-//-----------------------------------------------------------------------------
-int RichText::FindFormatStreamIndexForTextStreamPos(int textStreamIndex)
-{
- int formatStreamIndex = 0;
- for (; m_FormatStream.IsValidIndex(formatStreamIndex); formatStreamIndex++)
- {
- if (m_FormatStream[formatStreamIndex].textStreamIndex > textStreamIndex)
- break;
- }
-
- // step back to the color change before the new line
- formatStreamIndex--;
- if (!m_FormatStream.IsValidIndex(formatStreamIndex))
- {
- formatStreamIndex = 0;
- }
- return formatStreamIndex;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Generates a base renderstate given a index into the text stream
-//-----------------------------------------------------------------------------
-void RichText::GenerateRenderStateForTextStreamIndex(int textStreamIndex, TRenderState &renderState)
-{
- // find where in the format stream we need to be given the specified place in the text stream
- renderState.formatStreamIndex = FindFormatStreamIndexForTextStreamPos(textStreamIndex);
-
- // copy the state data
- renderState.textColor = m_FormatStream[renderState.formatStreamIndex].color;
- renderState.pixelsIndent = m_FormatStream[renderState.formatStreamIndex].pixelsIndent;
- renderState.textClickable = m_FormatStream[renderState.formatStreamIndex].textClickable;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Called pre render
-//-----------------------------------------------------------------------------
-void RichText::OnThink()
-{
- if (m_bRecalcLineBreaks)
- {
- _recalcSavedRenderState = true;
- RecalculateLineBreaks();
-
- // recalculate scrollbar position
- if (_invalidateVerticalScrollbarSlider)
- {
- LayoutVerticalScrollBarSlider();
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Called when data changes or panel size changes
-//-----------------------------------------------------------------------------
-void RichText::PerformLayout()
-{
- BaseClass::PerformLayout();
-
- // force a Repaint
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: inserts a color change into the formatting stream
-//-----------------------------------------------------------------------------
-void RichText::InsertColorChange(Color col)
-{
- // see if color already exists in text stream
- TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1];
- if (prevItem.color == col)
- {
- // inserting same color into stream, just ignore
- }
- else if (prevItem.textStreamIndex == m_TextStream.Count())
- {
- // this item is in the same place; update values
- prevItem.color = col;
- }
- else
- {
- // add to text stream, based off existing item
- TFormatStream streamItem = prevItem;
- streamItem.color = col;
- streamItem.textStreamIndex = m_TextStream.Count();
- m_FormatStream.AddToTail(streamItem);
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: inserts a fade into the formatting stream
-//-----------------------------------------------------------------------------
-void RichText::InsertFade( float flSustain, float flLength )
-{
- // see if color already exists in text stream
- TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1];
- if (prevItem.textStreamIndex == m_TextStream.Count())
- {
- // this item is in the same place; update values
- prevItem.fade.flFadeStartTime = system()->GetCurrentTime() + flSustain;
- prevItem.fade.flFadeSustain = flSustain;
- prevItem.fade.flFadeLength = flLength;
- prevItem.fade.iOriginalAlpha = prevItem.color.a();
- }
- else
- {
- // add to text stream, based off existing item
- TFormatStream streamItem = prevItem;
-
- prevItem.fade.flFadeStartTime = system()->GetCurrentTime() + flSustain;
- prevItem.fade.flFadeLength = flLength;
- prevItem.fade.flFadeSustain = flSustain;
- prevItem.fade.iOriginalAlpha = prevItem.color.a();
-
- streamItem.textStreamIndex = m_TextStream.Count();
- m_FormatStream.AddToTail(streamItem);
- }
-}
-
-void RichText::ResetAllFades( bool bHold, bool bOnlyExpired, float flNewSustain )
-{
- m_bResetFades = bHold;
-
- if ( m_bResetFades == false )
- {
- for (int i = 1; i < m_FormatStream.Count(); i++)
- {
- if ( bOnlyExpired == true )
- {
- if ( m_FormatStream[i].fade.flFadeStartTime >= system()->GetCurrentTime() )
- continue;
- }
-
- if ( flNewSustain == -1.0f )
- {
- flNewSustain = m_FormatStream[i].fade.flFadeSustain;
- }
-
- m_FormatStream[i].fade.flFadeStartTime = system()->GetCurrentTime() + flNewSustain;
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: inserts an indent change into the formatting stream
-//-----------------------------------------------------------------------------
-void RichText::InsertIndentChange(int pixelsIndent)
-{
- if (pixelsIndent < 0)
- {
- pixelsIndent = 0;
- }
- else if (pixelsIndent > 255)
- {
- pixelsIndent = 255;
- }
-
- // see if indent change already exists in text stream
- TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1];
- if (prevItem.pixelsIndent == pixelsIndent)
- {
- // inserting same indent into stream, just ignore
- }
- else if (prevItem.textStreamIndex == m_TextStream.Count())
- {
- // this item is in the same place; update
- prevItem.pixelsIndent = pixelsIndent;
- }
- else
- {
- // add to text stream, based off existing item
- TFormatStream streamItem = prevItem;
- streamItem.pixelsIndent = pixelsIndent;
- streamItem.textStreamIndex = m_TextStream.Count();
- m_FormatStream.AddToTail(streamItem);
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Inserts character Start for clickable text, eg. URLS
-//-----------------------------------------------------------------------------
-void RichText::InsertClickableTextStart( const char *pchClickAction )
-{
- // see if indent change already exists in text stream
- TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1];
- TFormatStream *pFormatStream = &prevItem;
- if (prevItem.textStreamIndex == m_TextStream.Count())
- {
- // this item is in the same place; update
- prevItem.textClickable = true;
- pFormatStream->m_sClickableTextAction = pchClickAction;
- }
- else
- {
- // add to text stream, based off existing item
- TFormatStream formatStreamCopy = prevItem;
- int iFormatStream = m_FormatStream.AddToTail( formatStreamCopy );
-
- // set the new params
- pFormatStream = &m_FormatStream[iFormatStream];
- pFormatStream->textStreamIndex = m_TextStream.Count();
- pFormatStream->textClickable = true;
- pFormatStream->m_sClickableTextAction = pchClickAction;
- }
-
- // invalidate the layout to recalculate where the click panels should go
- InvalidateLineBreakStream();
- InvalidateLayout();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Inserts character end for clickable text, eg. URLS
-//-----------------------------------------------------------------------------
-void RichText::InsertClickableTextEnd()
-{
- // see if indent change already exists in text stream
- TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1];
- if (!prevItem.textClickable)
- {
- // inserting same indent into stream, just ignore
- }
- else if (prevItem.textStreamIndex == m_TextStream.Count())
- {
- // this item is in the same place; update
- prevItem.textClickable = false;
- }
- else
- {
- // add to text stream, based off existing item
- TFormatStream streamItem = prevItem;
- streamItem.textClickable = false;
- streamItem.textStreamIndex = m_TextStream.Count();
- m_FormatStream.AddToTail(streamItem);
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: moves x,y to the Start of the next line of text
-//-----------------------------------------------------------------------------
-void RichText::AddAnotherLine(int &cx, int &cy)
-{
- cx = _drawOffsetX + _pixelsIndent;
- cy += (GetLineHeight() + _drawOffsetY);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Recalculates line breaks
-//-----------------------------------------------------------------------------
-void RichText::RecalculateLineBreaks()
-{
- if ( !m_bRecalcLineBreaks )
- return;
-
- int wide = GetWide();
- if (!wide)
- return;
-
- wide -= _drawOffsetX;
-
- m_bRecalcLineBreaks = false;
- _recalcSavedRenderState = true;
- if (!HasText())
- return;
-
- int selection0 = -1, selection1 = -1;
-
- // subtract the scrollbar width
- if (_vertScrollBar->IsVisible())
- {
- wide -= _vertScrollBar->GetWide();
- }
-
- int x = _drawOffsetX, y = _drawOffsetY;
-
- HFont fontWordStart = INVALID_FONT;
- int wordStartIndex = 0;
- int lineStartIndex = 0;
- bool hasWord = false;
- bool justStartedNewLine = true;
- bool wordStartedOnNewLine = true;
-
- int startChar = 0;
- if (_recalculateBreaksIndex <= 0)
- {
- m_LineBreaks.RemoveAll();
- }
- else
- {
- // remove the rest of the linebreaks list since its out of date.
- for (int i = _recalculateBreaksIndex + 1; i < m_LineBreaks.Count(); ++i)
- {
- m_LineBreaks.Remove(i);
- --i; // removing shrinks the list!
- }
- startChar = m_LineBreaks[_recalculateBreaksIndex];
- lineStartIndex = m_LineBreaks[_recalculateBreaksIndex];
- wordStartIndex = lineStartIndex;
- }
-
- // handle the case where this char is a new line, in that case
- // we have already taken its break index into account above so skip it.
- if (m_TextStream[startChar] == '\r' || m_TextStream[startChar] == '\n')
- {
- startChar++;
- lineStartIndex = startChar;
- }
-
- // cycle to the right url panel for what is visible after the startIndex.
- int clickableTextNum = GetClickableTextIndexStart(startChar);
- clickableTextNum++;
-
- // initialize the renderstate with the start
- TRenderState renderState;
- GenerateRenderStateForTextStreamIndex(startChar, renderState);
- _currentTextClickable = false;
-
- HFont font = _font;
-
- bool bForceBreak = false;
- float flLineWidthSoFar = 0;
-
- // loop through all the characters
- for (int i = startChar; i < m_TextStream.Count(); ++i)
- {
- wchar_t ch = m_TextStream[i];
- renderState.x = x;
- if (UpdateRenderState(i, renderState))
- {
- x = renderState.x;
- int preI = i;
-
- // check for clickable text
- if (renderState.textClickable != _currentTextClickable)
- {
- if (renderState.textClickable)
- {
- // make a new clickable text panel
- if (clickableTextNum >= _clickableTextPanels.Count())
- {
- _clickableTextPanels.AddToTail(new ClickPanel(this));
- }
-
- ClickPanel *clickPanel = _clickableTextPanels[clickableTextNum++];
- clickPanel->SetTextIndex(preI, preI);
- }
-
- // url state change
- _currentTextClickable = renderState.textClickable;
- }
- }
-
- bool bIsWSpace = iswspace( ch ) ? true : false;
-
- bool bPreviousWordStartedOnNewLine = wordStartedOnNewLine;
- int iPreviousWordStartIndex = wordStartIndex;
- if ( !bIsWSpace && ch != L'\t' && ch != L'\n' && ch != L'\r' )
- {
- if (!hasWord)
- {
- // Start a new word
- wordStartIndex = i;
- hasWord = true;
- wordStartedOnNewLine = justStartedNewLine;
- fontWordStart = font;
- }
- // else append to the current word
- }
- else
- {
- // whitespace/punctuation character
- // end the word
- hasWord = false;
- }
-
- float w = 0;
- wchar_t wchBefore = 0;
- wchar_t wchAfter = 0;
-
- if ( i > 0 && i > lineStartIndex && i != selection0 && i-1 != selection1 )
- wchBefore = m_TextStream[i-1];
- if ( i < m_TextStream.Count() - 1 && i+1 != selection0 && i != selection1 )
- wchAfter = m_TextStream[i+1];
-
- float flabcA;
- surface()->GetKernedCharWidth( font, ch, wchBefore, wchAfter, w, flabcA );
- flLineWidthSoFar += w;
-
- // See if we've exceeded the width we have available, with
- if ( floor(flLineWidthSoFar + 0.6) + x > wide )
- {
- bForceBreak = true;
- }
-
- if (!iswcntrl(ch))
- {
- justStartedNewLine = false;
- }
-
- if ( bForceBreak || ch == '\r' || ch == '\n' )
- {
- bForceBreak = false;
- // add another line
- AddAnotherLine(x, y);
-
- if ( ch == '\r' || ch == '\n' )
- {
- // skip the newline so it's not at the beginning of the new line
- lineStartIndex = i + 1;
- m_LineBreaks.AddToTail(i + 1);
- }
- else if ( bPreviousWordStartedOnNewLine || iPreviousWordStartIndex <= lineStartIndex )
- {
- lineStartIndex = i;
- m_LineBreaks.AddToTail( i );
-
- if (renderState.textClickable)
- {
- // need to split the url into two panels
- int oldIndex = _clickableTextPanels[clickableTextNum - 1]->GetTextIndex();
-
- // make a new clickable text panel
- if (clickableTextNum >= _clickableTextPanels.Count())
- {
- _clickableTextPanels.AddToTail(new ClickPanel(this));
- }
-
- ClickPanel *clickPanel = _clickableTextPanels[clickableTextNum++];
- clickPanel->SetTextIndex(oldIndex, i);
- }
- }
- else
- {
- m_LineBreaks.AddToTail( iPreviousWordStartIndex );
- lineStartIndex = iPreviousWordStartIndex;
- i = iPreviousWordStartIndex;
-
- TRenderState renderStateAtLastWord;
- GenerateRenderStateForTextStreamIndex( i, renderStateAtLastWord );
-
- // If the word is clickable, and that started prior to the beginning of the word, then we must split the click panel
- if ( renderStateAtLastWord.textClickable && m_FormatStream[ renderStateAtLastWord.formatStreamIndex ].textStreamIndex < i )
- {
- // need to split the url into two panels
- int oldIndex = _clickableTextPanels[clickableTextNum - 1]->GetTextIndex();
-
- // make a new clickable text panel
- if (clickableTextNum >= _clickableTextPanels.Count())
- {
- _clickableTextPanels.AddToTail(new ClickPanel(this));
- }
-
- ClickPanel *clickPanel = _clickableTextPanels[clickableTextNum++];
- clickPanel->SetTextIndex(oldIndex, i);
- }
- }
-
- flLineWidthSoFar = 0;
- justStartedNewLine = true;
- hasWord = false;
- wordStartedOnNewLine = false;
- _currentTextClickable = false;
- continue;
- }
- }
-
- // end the list
- m_LineBreaks.AddToTail(MAX_BUFFER_SIZE);
-
- // set up the scrollbar
- _invalidateVerticalScrollbarSlider = true;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Recalculate where the vertical scroll bar slider should be
-// based on the current cursor line we are on.
-//-----------------------------------------------------------------------------
-void RichText::LayoutVerticalScrollBarSlider()
-{
- _invalidateVerticalScrollbarSlider = false;
-
- // set up the scrollbar
- //if (!_vertScrollBar->IsVisible())
- // return;
-
-
- // see where the scrollbar currently is
- int previousValue = _vertScrollBar->GetValue();
- bool bCurrentlyAtEnd = false;
- int rmin, rmax;
- _vertScrollBar->GetRange(rmin, rmax);
- if (rmax && (previousValue + rmin + _vertScrollBar->GetRangeWindow() == rmax))
- {
- bCurrentlyAtEnd = true;
- }
-
- // work out position to put scrollbar, factoring in insets
- int wide, tall;
- GetSize( wide, tall );
-
- _vertScrollBar->SetPos( wide - _vertScrollBar->GetWide(), 0 );
- // scrollbar is inside the borders.
- _vertScrollBar->SetSize( _vertScrollBar->GetWide(), tall );
-
- // calculate how many lines we can fully display
- int displayLines = tall / (GetLineHeight() + _drawOffsetY);
- int numLines = m_LineBreaks.Count();
-
- if (numLines <= displayLines)
- {
- // disable the scrollbar
- _vertScrollBar->SetEnabled(false);
- _vertScrollBar->SetRange(0, numLines);
- _vertScrollBar->SetRangeWindow(numLines);
- _vertScrollBar->SetValue(0);
-
- if ( m_bUnusedScrollbarInvis )
- {
- SetVerticalScrollbar( false );
- }
- }
- else
- {
- if ( m_bUnusedScrollbarInvis )
- {
- SetVerticalScrollbar( true );
- }
-
- // set the scrollbars range
- _vertScrollBar->SetRange(0, numLines);
- _vertScrollBar->SetRangeWindow(displayLines);
- _vertScrollBar->SetEnabled(true);
-
- // this should make it scroll one line at a time
- _vertScrollBar->SetButtonPressedScrollValue(1);
- if (bCurrentlyAtEnd)
- {
- _vertScrollBar->SetValue(numLines - displayLines);
- }
- _vertScrollBar->InvalidateLayout();
- _vertScrollBar->Repaint();
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Sets whether a vertical scrollbar is visible
-//-----------------------------------------------------------------------------
-void RichText::SetVerticalScrollbar(bool state)
-{
- if (_vertScrollBar->IsVisible() != state)
- {
- _vertScrollBar->SetVisible(state);
- InvalidateLineBreakStream();
- InvalidateLayout();
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Create cut/copy/paste dropdown menu
-//-----------------------------------------------------------------------------
-void RichText::CreateEditMenu()
-{
- // create a drop down cut/copy/paste menu appropriate for this object's states
- if (m_pEditMenu)
- delete m_pEditMenu;
- m_pEditMenu = new Menu(this, "EditMenu");
-
-
- // add cut/copy/paste drop down options if its editable, just copy if it is not
- m_pEditMenu->AddMenuItem("C&opy", new KeyValues("DoCopySelected"), this);
-
- m_pEditMenu->SetVisible(false);
- m_pEditMenu->SetParent(this);
- m_pEditMenu->AddActionSignalTarget(this);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: We want single line windows to scroll horizontally and select text
-// in response to clicking and holding outside window
-//-----------------------------------------------------------------------------
-void RichText::OnMouseFocusTicked()
-{
- // if a button is down move the scrollbar slider the appropriate direction
- if (_mouseDragSelection) // text is being selected via mouse clicking and dragging
- {
- OnCursorMoved(0,0); // we want the text to scroll as if we were dragging
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: If a cursor enters the window, we are not elegible for
-// MouseFocusTicked events
-//-----------------------------------------------------------------------------
-void RichText::OnCursorEntered()
-{
- _mouseDragSelection = false; // outside of window dont recieve drag scrolling ticks
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: When the cursor is outside the window, if we are holding the mouse
-// button down, then we want the window to scroll the text one char at a time using Ticks
-//-----------------------------------------------------------------------------
-void RichText::OnCursorExited()
-{
- // outside of window recieve drag scrolling ticks
- if (_mouseSelection)
- {
- _mouseDragSelection = true;
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Handle selection of text by mouse
-//-----------------------------------------------------------------------------
-void RichText::OnCursorMoved(int x, int y)
-{
- if (_mouseSelection)
- {
- // update the cursor position
- int x, y;
- input()->GetCursorPos(x, y);
- ScreenToLocal(x, y);
- _cursorPos = PixelToCursorSpace(x, y);
-
- if (_cursorPos != _select[1])
- {
- _select[1] = _cursorPos;
- Repaint();
- }
- // Msg( "selecting range [%d..%d]\n", _select[0], _select[1] );
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Handle mouse button down events.
-//-----------------------------------------------------------------------------
-void RichText::OnMousePressed(MouseCode code)
-{
- if (code == MOUSE_LEFT)
- {
- // clear current selection
- SelectNone();
-
- // move the cursor to where the mouse was pressed
- int x, y;
- input()->GetCursorPos(x, y);
- ScreenToLocal(x, y);
-
- _cursorPos = PixelToCursorSpace(x, y);
-
- if ( m_bInteractive )
- {
- // enter selection mode
- input()->SetMouseCapture(GetVPanel());
- _mouseSelection = true;
-
- if (_select[0] < 0)
- {
- // if no initial selection position, Start selection position at cursor
- _select[0] = _cursorPos;
- }
- _select[1] = _cursorPos;
- }
-
- RequestFocus();
- Repaint();
- }
- else if (code == MOUSE_RIGHT) // check for context menu open
- {
- if ( m_bInteractive )
- {
- CreateEditMenu();
- Assert(m_pEditMenu);
-
- OpenEditMenu();
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Handle mouse button up events
-//-----------------------------------------------------------------------------
-void RichText::OnMouseReleased(MouseCode code)
-{
- _mouseSelection = false;
- input()->SetMouseCapture(NULL);
-
- // make sure something has been selected
- int cx0, cx1;
- if (GetSelectedRange(cx0, cx1))
- {
- if (cx1 - cx0 == 0)
- {
- // nullify selection
- _select[0] = -1;
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Handle mouse double clicks
-//-----------------------------------------------------------------------------
-void RichText::OnMouseDoublePressed(MouseCode code)
-{
- if ( !m_bInteractive )
- return;
-
- // left double clicking on a word selects the word
- if (code == MOUSE_LEFT)
- {
- // move the cursor just as if you single clicked.
- OnMousePressed(code);
- // then find the start and end of the word we are in to highlight it.
- int selectSpot[2];
- GotoWordLeft();
- selectSpot[0] = _cursorPos;
- GotoWordRight();
- selectSpot[1] = _cursorPos;
-
- if ( _cursorPos > 0 && (_cursorPos-1) < m_TextStream.Count() )
- {
- if (iswspace(m_TextStream[_cursorPos-1]))
- {
- selectSpot[1]--;
- _cursorPos--;
- }
- }
-
- _select[0] = selectSpot[0];
- _select[1] = selectSpot[1];
- _mouseSelection = true;
- }
-
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Turn off text selection code when mouse button is not down
-//-----------------------------------------------------------------------------
-void RichText::OnMouseCaptureLost()
-{
- _mouseSelection = false;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Masks which keys get chained up
-// Maps keyboard input to text window functions.
-//-----------------------------------------------------------------------------
-void RichText::OnKeyCodeTyped(KeyCode code)
-{
- bool shift = (input()->IsKeyDown(KEY_LSHIFT) || input()->IsKeyDown(KEY_RSHIFT));
- bool ctrl = (input()->IsKeyDown(KEY_LCONTROL) || input()->IsKeyDown(KEY_RCONTROL));
- bool alt = (input()->IsKeyDown(KEY_LALT) || input()->IsKeyDown(KEY_RALT));
- bool winkey = (input()->IsKeyDown(KEY_LWIN) || input()->IsKeyDown(KEY_RWIN));
- bool fallThrough = false;
-
- if ( ctrl || ( winkey && IsOSX() ) )
- {
- switch(code)
- {
- case KEY_INSERT:
- case KEY_C:
- case KEY_X:
- {
- CopySelected();
- break;
- }
- case KEY_PAGEUP:
- case KEY_HOME:
- {
- GotoTextStart();
- break;
- }
- case KEY_PAGEDOWN:
- case KEY_END:
- {
- GotoTextEnd();
- break;
- }
- default:
- {
- fallThrough = true;
- break;
- }
- }
- }
- else if (alt)
- {
- // do nothing with ALT-x keys
- fallThrough = true;
- }
- else
- {
- switch(code)
- {
- case KEY_TAB:
- case KEY_LSHIFT:
- case KEY_RSHIFT:
- case KEY_ESCAPE:
- case KEY_ENTER:
- {
- fallThrough = true;
- break;
- }
- case KEY_DELETE:
- {
- if (shift)
- {
- // shift-delete is cut
- CopySelected();
- }
- break;
- }
- case KEY_HOME:
- {
- GotoTextStart();
- break;
- }
- case KEY_END:
- {
- GotoTextEnd();
- break;
- }
- case KEY_PAGEUP:
- {
- // if there is a scroll bar scroll down one rangewindow
- if (_vertScrollBar->IsVisible())
- {
- int window = _vertScrollBar->GetRangeWindow();
- int newval = _vertScrollBar->GetValue();
- _vertScrollBar->SetValue(newval - window - 1);
- }
- break;
-
- }
- case KEY_PAGEDOWN:
- {
- // if there is a scroll bar scroll down one rangewindow
- if (_vertScrollBar->IsVisible())
- {
- int window = _vertScrollBar->GetRangeWindow();
- int newval = _vertScrollBar->GetValue();
- _vertScrollBar->SetValue(newval + window + 1);
- }
- break;
- }
- default:
- {
- // return if any other char is pressed.
- // as it will be a unicode char.
- // and we don't want select[1] changed unless a char was pressed that this fxn handles
- return;
- }
- }
- }
-
- // select[1] is the location in the line where the blinking cursor started
- _select[1] = _cursorPos;
-
- // chain back on some keys
- if (fallThrough)
- {
- BaseClass::OnKeyCodeTyped(code);
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Scrolls the list according to the mouse wheel movement
-//-----------------------------------------------------------------------------
-void RichText::OnMouseWheeled(int delta)
-{
- MoveScrollBar(delta);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Scrolls the list
-// Input : delta - amount to move scrollbar up
-//-----------------------------------------------------------------------------
-void RichText::MoveScrollBar(int delta)
-{
- MoveScrollBarDirect( delta * 3 );
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Scrolls the list
-// Input : delta - amount to move scrollbar up
-//-----------------------------------------------------------------------------
-void RichText::MoveScrollBarDirect(int delta)
-{
- if (_vertScrollBar->IsVisible())
- {
- int val = _vertScrollBar->GetValue();
- val -= delta;
- _vertScrollBar->SetValue(val);
- _recalcSavedRenderState = true;
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: set the maximum number of chars in the text buffer
-//-----------------------------------------------------------------------------
-void RichText::SetMaximumCharCount(int maxChars)
-{
- _maxCharCount = maxChars;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Find out what line the cursor is on
-//-----------------------------------------------------------------------------
-int RichText::GetCursorLine()
-{
- // always returns the last place
- int pos = m_LineBreaks[m_LineBreaks.Count() - 1];
- Assert(pos == MAX_BUFFER_SIZE);
- return pos;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Move the cursor over to the Start of the next word to the right
-//-----------------------------------------------------------------------------
-void RichText::GotoWordRight()
-{
- // search right until we hit a whitespace character or a newline
- while (++_cursorPos < m_TextStream.Count())
- {
- if (iswspace(m_TextStream[_cursorPos]))
- break;
- }
-
- // search right until we hit an nonspace character
- while (++_cursorPos < m_TextStream.Count())
- {
- if (!iswspace(m_TextStream[_cursorPos]))
- break;
- }
-
- if (_cursorPos > m_TextStream.Count())
- {
- _cursorPos = m_TextStream.Count();
- }
-
- // now we are at the start of the next word
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Move the cursor over to the Start of the next word to the left
-//-----------------------------------------------------------------------------
-void RichText::GotoWordLeft()
-{
- if (_cursorPos < 1)
- return;
-
- // search left until we hit an nonspace character
- while (--_cursorPos >= 0)
- {
- if (!iswspace(m_TextStream[_cursorPos]))
- break;
- }
-
- // search left until we hit a whitespace character
- while (--_cursorPos >= 0)
- {
- if (iswspace(m_TextStream[_cursorPos]))
- {
- break;
- }
- }
-
- // we end one character off
- _cursorPos++;
-
- // now we are at the start of the previous word
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Move cursor to the Start of the text buffer
-//-----------------------------------------------------------------------------
-void RichText::GotoTextStart()
-{
- _cursorPos = 0; // set cursor to start
- _invalidateVerticalScrollbarSlider = true;
- // force scrollbar to the top
- _vertScrollBar->SetValue(0);
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Move cursor to the end of the text buffer
-//-----------------------------------------------------------------------------
-void RichText::GotoTextEnd()
-{
- _cursorPos = m_TextStream.Count(); // set cursor to end of buffer
- _invalidateVerticalScrollbarSlider = true;
-
- // force the scrollbar to the bottom
- int min, max;
- _vertScrollBar->GetRange(min, max);
- _vertScrollBar->SetValue(max);
-
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Culls the text stream down to a managable size
-//-----------------------------------------------------------------------------
-void RichText::TruncateTextStream()
-{
- if (_maxCharCount < 1)
- return;
-
- // choose a point to cull at
- int cullPos = _maxCharCount / 2;
-
- // kill half the buffer
- m_TextStream.RemoveMultiple(0, cullPos);
-
- // work out where in the format stream we can start
- int formatIndex = FindFormatStreamIndexForTextStreamPos(cullPos);
- if (formatIndex > 0)
- {
- // take a copy, make it first
- m_FormatStream[0] = m_FormatStream[formatIndex];
- m_FormatStream[0].textStreamIndex = 0;
- // kill the others
- m_FormatStream.RemoveMultiple(1, formatIndex);
- }
-
- // renormalize the remainder of the format stream
- for (int i = 1; i < m_FormatStream.Count(); i++)
- {
- Assert(m_FormatStream[i].textStreamIndex > cullPos);
- m_FormatStream[i].textStreamIndex -= cullPos;
- }
-
- // mark everything to be recalculated
- InvalidateLineBreakStream();
- InvalidateLayout();
- _invalidateVerticalScrollbarSlider = true;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Insert a character into the text buffer
-//-----------------------------------------------------------------------------
-void RichText::InsertChar(wchar_t wch)
-{
- // throw away redundant linefeed characters
- if ( wch == '\r' )
- return;
-
- if (_maxCharCount > 0 && m_TextStream.Count() > _maxCharCount)
- {
- TruncateTextStream();
- }
-
- // insert the new char at the end of the buffer
- m_TextStream.AddToTail(wch);
-
- // mark the linebreak steam as needing recalculating from that point
- _recalculateBreaksIndex = m_LineBreaks.Count() - 2;
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Insert a string into the text buffer, this is just a series
-// of char inserts because we have to check each char is ok to insert
-//-----------------------------------------------------------------------------
-void RichText::InsertString(const char *text)
-{
- if (text[0] == '#')
- {
- wchar_t unicode[ 1024 ];
- ResolveLocalizedTextAndVariables( text, unicode, sizeof( unicode ) );
- InsertString( unicode );
- return;
- }
-
- // upgrade the ansi text to unicode to display it
- int len = strlen(text);
- wchar_t *unicode = (wchar_t *)_alloca((len + 1) * sizeof(wchar_t));
- Q_UTF8ToUnicode(text, unicode, ((len + 1) * sizeof(wchar_t)));
- InsertString(unicode);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Insertsa a unicode string into the buffer
-//-----------------------------------------------------------------------------
-void RichText::InsertString(const wchar_t *wszText)
-{
- // insert the whole string
- for (const wchar_t *ch = wszText; *ch != 0; ++ch)
- {
- InsertChar(*ch);
- }
- InvalidateLayout();
- m_bRecalcLineBreaks = true;
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Declare a selection empty
-//-----------------------------------------------------------------------------
-void RichText::SelectNone()
-{
- // tag the selection as empty
- _select[0] = -1;
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Load in the selection range so cx0 is the Start and cx1 is the end
-// from smallest to highest (right to left)
-//-----------------------------------------------------------------------------
-bool RichText::GetSelectedRange(int &cx0, int &cx1)
-{
- // if there is nothing selected return false
- if (_select[0] == -1)
- return false;
-
- // sort the two position so cx0 is the smallest
- cx0 = _select[0];
- cx1 = _select[1];
- if (cx1 < cx0)
- {
- int temp = cx0;
- cx0 = cx1;
- cx1 = temp;
- }
-
- return true;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Opens the cut/copy/paste dropdown menu
-//-----------------------------------------------------------------------------
-void RichText::OpenEditMenu()
-{
- // get cursor position, this is local to this text edit window
- // so we need to adjust it relative to the parent
- int cursorX, cursorY;
- input()->GetCursorPos(cursorX, cursorY);
-
- /* !! disabled since it recursively gets panel pointers, potentially across dll boundaries,
- and doesn't need to be necessary (it's just for handling windowed mode)
-
- // find the frame that has no parent (the one on the desktop)
- Panel *panel = this;
- while ( panel->GetParent() != NULL)
- {
- panel = panel->GetParent();
- }
- panel->ScreenToLocal(cursorX, cursorY);
- int x, y;
- // get base panel's postition
- panel->GetPos(x, y);
-
- // adjust our cursor position accordingly
- cursorX += x;
- cursorY += y;
- */
-
- int x0, x1;
- if (GetSelectedRange(x0, x1)) // there is something selected
- {
- m_pEditMenu->SetItemEnabled("&Cut", true);
- m_pEditMenu->SetItemEnabled("C&opy", true);
- }
- else // there is nothing selected, disable cut/copy options
- {
- m_pEditMenu->SetItemEnabled("&Cut", false);
- m_pEditMenu->SetItemEnabled("C&opy", false);
- }
- m_pEditMenu->SetVisible(true);
- m_pEditMenu->RequestFocus();
-
- // relayout the menu immediately so that we know it's size
- m_pEditMenu->InvalidateLayout(true);
- int menuWide, menuTall;
- m_pEditMenu->GetSize(menuWide, menuTall);
-
- // work out where the cursor is and therefore the best place to put the menu
- int wide, tall;
- surface()->GetScreenSize(wide, tall);
-
- if (wide - menuWide > cursorX)
- {
- // menu hanging right
- if (tall - menuTall > cursorY)
- {
- // menu hanging down
- m_pEditMenu->SetPos(cursorX, cursorY);
- }
- else
- {
- // menu hanging up
- m_pEditMenu->SetPos(cursorX, cursorY - menuTall);
- }
- }
- else
- {
- // menu hanging left
- if (tall - menuTall > cursorY)
- {
- // menu hanging down
- m_pEditMenu->SetPos(cursorX - menuWide, cursorY);
- }
- else
- {
- // menu hanging up
- m_pEditMenu->SetPos(cursorX - menuWide, cursorY - menuTall);
- }
- }
-
- m_pEditMenu->RequestFocus();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Cuts the selected chars from the buffer and
-// copies them into the clipboard
-//-----------------------------------------------------------------------------
-void RichText::CutSelected()
-{
- CopySelected();
- // have to request focus if we used the menu
- RequestFocus();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Copies the selected chars into the clipboard
-//-----------------------------------------------------------------------------
-void RichText::CopySelected()
-{
- int x0, x1;
- if (GetSelectedRange(x0, x1))
- {
- CUtlVector<wchar_t> buf;
- for (int i = x0; i <= x1; i++)
- {
- if ( m_TextStream.IsValidIndex(i) == false )
- continue;
-
- if (m_TextStream[i] == '\n')
- {
- buf.AddToTail( '\r' );
- }
- // remove any rich edit commands
- buf.AddToTail(m_TextStream[i]);
- }
- buf.AddToTail('\0');
- system()->SetClipboardText(buf.Base(), buf.Count() - 1);
- }
-
- // have to request focus if we used the menu
- RequestFocus();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Returns the index in the text buffer of the
-// character the drawing should Start at
-//-----------------------------------------------------------------------------
-int RichText::GetStartDrawIndex(int &lineBreakIndexIndex)
-{
- int startIndex = 0;
- int startLine = _vertScrollBar->GetValue();
-
- if ( startLine >= m_LineBreaks.Count() ) // incase the line breaks got reset and the scroll bar hasn't
- {
- startLine = m_LineBreaks.Count() - 1;
- }
-
- lineBreakIndexIndex = startLine;
- if (startLine && startLine < m_LineBreaks.Count())
- {
- startIndex = m_LineBreaks[startLine - 1];
- }
-
- return startIndex;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Get a string from text buffer
-// Input: offset - index to Start reading from
-// bufLen - length of string
-//-----------------------------------------------------------------------------
-void RichText::GetText(int offset, wchar_t *buf, int bufLenInBytes)
-{
- if (!buf)
- return;
-
- Assert( bufLenInBytes >= sizeof(buf[0]) );
- int bufLen = bufLenInBytes / sizeof(wchar_t);
- int i;
- for (i = offset; i < (offset + bufLen - 1); i++)
- {
- if (i >= m_TextStream.Count())
- break;
-
- buf[i-offset] = m_TextStream[i];
- }
- buf[(i-offset)] = 0;
- buf[bufLen-1] = 0;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: gets text from the buffer
-//-----------------------------------------------------------------------------
-void RichText::GetText(int offset, char *pch, int bufLenInBytes)
-{
- wchar_t rgwchT[4096];
- GetText(offset, rgwchT, sizeof(rgwchT));
- Q_UnicodeToUTF8(rgwchT, pch, bufLenInBytes);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Set the font of the buffer text
-//-----------------------------------------------------------------------------
-void RichText::SetFont(HFont font)
-{
- _font = font;
- InvalidateLayout();
- m_bRecalcLineBreaks = true;
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Called when the scrollbar slider is moved
-//-----------------------------------------------------------------------------
-void RichText::OnSliderMoved()
-{
- _recalcSavedRenderState = true;
- Repaint();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-bool RichText::RequestInfo(KeyValues *outputData)
-{
- if (!stricmp(outputData->GetName(), "GetText"))
- {
- wchar_t wbuf[512];
- GetText(0, wbuf, sizeof(wbuf));
- outputData->SetWString("text", wbuf);
- return true;
- }
-
- return BaseClass::RequestInfo(outputData);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::OnSetText(const wchar_t *text)
-{
- SetText(text);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Called when a URL, etc has been clicked on
-//-----------------------------------------------------------------------------
-void RichText::OnClickPanel(int index)
-{
- wchar_t wBuf[512];
- int outIndex = 0;
-
- // parse out the clickable text, and send it to our listeners
- _currentTextClickable = true;
- TRenderState renderState;
- GenerateRenderStateForTextStreamIndex(index, renderState);
- for (int i = index; i < (sizeof(wBuf) - 1) && i < m_TextStream.Count(); i++)
- {
- // stop getting characters when text is no longer clickable
- UpdateRenderState(i, renderState);
- if (!renderState.textClickable)
- break;
-
- // copy out the character
- wBuf[outIndex++] = m_TextStream[i];
- }
-
- wBuf[outIndex] = 0;
-
- int iFormatSteam = FindFormatStreamIndexForTextStreamPos( index );
- if ( m_FormatStream[iFormatSteam].m_sClickableTextAction )
- {
- Q_UTF8ToUnicode( m_FormatStream[iFormatSteam].m_sClickableTextAction.String(), wBuf, sizeof( wBuf ) );
- }
-
- PostActionSignal(new KeyValues("TextClicked", "text", wBuf));
- OnTextClicked(wBuf);
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::ApplySettings(KeyValues *inResourceData)
-{
- BaseClass::ApplySettings(inResourceData);
- SetMaximumCharCount(inResourceData->GetInt("maxchars", -1));
- SetVerticalScrollbar(inResourceData->GetInt("scrollbar", 1));
-
- // get the starting text, if any
- const char *text = inResourceData->GetString("text", "");
- if (*text)
- {
- delete [] m_pszInitialText;
- int len = Q_strlen(text) + 1;
- m_pszInitialText = new char[ len ];
- Q_strncpy( m_pszInitialText, text, len );
- SetText(text);
- }
- else
- {
- const char *textfilename = inResourceData->GetString("textfile", NULL);
- if ( textfilename )
- {
- FileHandle_t f = g_pFullFileSystem->Open( textfilename, "rt" );
- if (!f)
- {
- Warning( "RichText: textfile parameter '%s' not found.\n", textfilename );
- return;
- }
-
- int len = g_pFullFileSystem->Size( f );
- delete [] m_pszInitialText;
- m_pszInitialText = new char[ len + 1 ];
- g_pFullFileSystem->Read( m_pszInitialText, len, f );
- m_pszInitialText[len - 1] = 0;
- SetText( m_pszInitialText );
-
- g_pFullFileSystem->Close( f );
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::GetSettings(KeyValues *outResourceData)
-{
- BaseClass::GetSettings(outResourceData);
- outResourceData->SetInt("maxchars", _maxCharCount);
- outResourceData->SetInt("scrollbar", _vertScrollBar->IsVisible() );
- if (m_pszInitialText)
- {
- outResourceData->SetString("text", m_pszInitialText);
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-const char *RichText::GetDescription()
-{
- static char buf[1024];
- Q_snprintf(buf, sizeof(buf), "%s, string text, bool scrollbar", BaseClass::GetDescription());
- return buf;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Get the number of lines in the window
-//-----------------------------------------------------------------------------
-int RichText::GetNumLines()
-{
- return m_LineBreaks.Count();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Sets the height of the text entry window so all text will fit inside
-//-----------------------------------------------------------------------------
-void RichText::SetToFullHeight()
-{
- PerformLayout();
- int wide, tall;
- GetSize(wide, tall);
-
- tall = GetNumLines() * (GetLineHeight() + _drawOffsetY) + _drawOffsetY + 2;
- SetSize (wide, tall);
- PerformLayout();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Select all the text.
-//-----------------------------------------------------------------------------
-void RichText::SelectAllText()
-{
- _cursorPos = 0;
- _select[0] = 0;
- _select[1] = m_TextStream.Count();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Select all the text.
-//-----------------------------------------------------------------------------
-void RichText::SelectNoText()
-{
- _select[0] = 0;
- _select[1] = 0;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::OnSetFocus()
-{
- BaseClass::OnSetFocus();
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Invalidates the current linebreak stream
-//-----------------------------------------------------------------------------
-void RichText::InvalidateLineBreakStream()
-{
- // clear the buffer
- m_LineBreaks.RemoveAll();
- m_LineBreaks.AddToTail(MAX_BUFFER_SIZE);
- _recalculateBreaksIndex = 0;
- m_bRecalcLineBreaks = true;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Inserts a text string while making URLs clickable/different color
-// Input : *text - string that may contain URLs to make clickable/color coded
-// URLTextColor - color for URL text
-// normalTextColor - color for normal text
-//-----------------------------------------------------------------------------
-void RichText::InsertPossibleURLString(const char* text, Color URLTextColor, Color normalTextColor)
-{
- InsertColorChange(normalTextColor);
-
- // parse out the string for URL's
- int len = Q_strlen(text), pos = 0;
- bool clickable = false;
- char *pchURLText = (char *)stackalloc( len + 1 );
- char *pchURL = (char *)stackalloc( len + 1 );
-
- while (pos < len)
- {
- pos = ParseTextStringForUrls( text, pos, pchURLText, len, pchURL, len, clickable );
-
- if ( clickable )
- {
- InsertClickableTextStart( pchURL );
- InsertColorChange( URLTextColor );
- }
-
- InsertString( pchURLText );
-
- if ( clickable )
- {
- InsertColorChange(normalTextColor);
- InsertClickableTextEnd();
- }
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: looks for URLs in the string and returns information about the URL
-//-----------------------------------------------------------------------------
-int RichText::ParseTextStringForUrls( const char *text, int startPos, char *pchURLText, int cchURLText, char *pchURL, int cchURL, bool &clickable )
-{
- // scan for text that looks like a URL
- int i = startPos;
- while (text[i] != 0)
- {
- bool bURLFound = false;
-
- if ( !Q_strnicmp(text + i, "<a href=", 8) )
- {
- if (i > startPos)
- break;
-
- // embedded link
- bURLFound = true;
- clickable = true;
- // get the url
- i += Q_strlen( "<a href=" );
- const char *pchURLEnd = Q_strstr( text + i, ">" );
- Q_strncpy( pchURL, text + i, min( pchURLEnd - text - i + 1, cchURL ) );
- i += ( pchURLEnd - text - i + 1 );
-
- // get the url text
- pchURLEnd = Q_strstr( text, "</a>" );
- Q_strncpy( pchURLText, text + i, min( pchURLEnd - text - i + 1, cchURLText ) );
- i += ( pchURLEnd - text - i );
- i += Q_strlen( "</a>" );
-
- // we're done
- return i;
- }
- else if (!Q_strnicmp(text + i, "www.", 4))
- {
- // scan ahead for another '.'
- bool bPeriodFound = false;
- for (const char *ch = text + i + 5; ch != 0; ch++)
- {
- if (*ch == '.')
- {
- bPeriodFound = true;
- break;
- }
- }
-
- // URL found
- if (bPeriodFound)
- {
- bURLFound = true;
- }
- }
- else if (!Q_strnicmp(text + i, "http://", 7))
- {
- bURLFound = true;
- }
- else if (!Q_strnicmp(text + i, "ftp://", 6))
- {
- bURLFound = true;
- }
- else if (!Q_strnicmp(text + i, "steam://", 8))
- {
- bURLFound = true;
- }
- else if (!Q_strnicmp(text + i, "steambeta://", 12))
- {
- bURLFound = true;
- }
- else if (!Q_strnicmp(text + i, "mailto:", 7))
- {
- bURLFound = true;
- }
- else if (!Q_strnicmp(text + i, "\\\\", 2))
- {
- bURLFound = true;
- }
-
- if (bURLFound)
- {
- if (i == startPos)
- {
- // we're at the Start of a URL, so parse that out
- clickable = true;
- int outIndex = 0;
- while (text[i] != 0 && !iswspace(text[i]))
- {
- pchURLText[outIndex++] = text[i++];
- }
- pchURLText[outIndex] = 0;
- Q_strncpy( pchURL, pchURLText, cchURL );
- return i;
- }
- else
- {
- // no url
- break;
- }
- }
-
- // increment and loop
- i++;
- }
-
- // nothing found;
- // parse out the text before the end
- clickable = false;
- int outIndex = 0;
- int fromIndex = startPos;
- while ( fromIndex < i && outIndex < cchURLText )
- {
- pchURLText[outIndex++] = text[fromIndex++];
- }
- pchURLText[outIndex] = 0;
- Q_strncpy( pchURL, pchURLText, cchURL );
-
- return i;
-}
-
-//-----------------------------------------------------------------------------
-// Purpose: Executes the text-clicked command, which opens a web browser by
-// default.
-//-----------------------------------------------------------------------------
-void RichText::OnTextClicked(const wchar_t *wszText)
-{
- // Strip leading/trailing quotes, which may be present on href tags or may not.
- const wchar_t *pwchURL = wszText;
- if ( pwchURL[0] == L'"' || pwchURL[0] == L'\'' )
- pwchURL = wszText + 1;
-
- char ansi[2048];
- Q_UnicodeToUTF8( pwchURL, ansi, sizeof(ansi) );
-
- size_t strLen = Q_strlen(ansi);
- if ( strLen && ( ansi[strLen-1] == '"' || ansi[strLen] == '\'' ) )
- {
- ansi[strLen-1] = 0;
- }
-
- if ( m_hPanelToHandleClickingURLs.Get() )
- {
- PostMessage( m_hPanelToHandleClickingURLs.Get(), new KeyValues( "URLClicked", "url", ansi ) );
- }
- else
- {
- system()->ShellExecute( "open", ansi );
- }
-}
-
-//-----------------------------------------------------------------------------
-// Purpose:
-//-----------------------------------------------------------------------------
-void RichText::SetURLClickedHandler( Panel *pPanelToHandleClickMsg )
-{
- m_hPanelToHandleClickingURLs = pPanelToHandleClickMsg;
-}
-
-
-//-----------------------------------------------------------------------------
-// Purpose: data accessor
-//-----------------------------------------------------------------------------
-bool RichText::IsScrollbarVisible()
-{
- return _vertScrollBar->IsVisible();
-}
-
-void RichText::SetUnderlineFont( HFont font )
-{
- m_hFontUnderline = font;
-}
-
-bool RichText::IsAllTextAlphaZero() const
-{
- return m_bAllTextAlphaIsZero;
-}
-
-bool RichText::HasText() const
-{
- int c = m_TextStream.Count();
- if ( c == 0 )
- {
- return false;
- }
- return true;
-}
-
-
-
-//-----------------------------------------------------------------------------
-// Purpose: Returns the height of the base font
-//-----------------------------------------------------------------------------
-int RichText::GetLineHeight()
-{
- return surface()->GetFontTall( _font );
-}
-
-
-#ifdef DBGFLAG_VALIDATE
-//-----------------------------------------------------------------------------
-// Purpose: Run a global validation pass on all of our data structures and memory
-// allocations.
-// Input: validator - Our global validator object
-// pchName - Our name (typically a member var in our container)
-//-----------------------------------------------------------------------------
-void RichText::Validate( CValidator &validator, char *pchName )
-{
- validator.Push( "vgui::RichText", this, pchName );
-
- ValidateObj( m_TextStream );
- ValidateObj( m_FormatStream );
- ValidateObj( m_LineBreaks );
- ValidateObj( _clickableTextPanels );
- validator.ClaimMemory( m_pszInitialText );
-
- BaseClass::Validate( validator, "vgui::RichText" );
-
- validator.Pop();
-}
-#endif // DBGFLAG_VALIDATE
-
+//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "vgui_controls/pch_vgui_controls.h" +#include "vgui/ILocalize.h" + +// memdbgon must be the last include file in a .cpp file +#include "tier0/memdbgon.h" + +enum +{ + MAX_BUFFER_SIZE = 999999, // maximum size of text buffer + DRAW_OFFSET_X = 3, + DRAW_OFFSET_Y = 1, +}; + +using namespace vgui; + +#ifndef max +#define max(a,b) (((a) > (b)) ? (a) : (b)) +#endif + +namespace vgui +{ + +//#define DRAW_CLICK_PANELS + +//----------------------------------------------------------------------------- +// Purpose: Panel used for clickable URL's +//----------------------------------------------------------------------------- +class ClickPanel : public Panel +{ + DECLARE_CLASS_SIMPLE( ClickPanel, Panel ); + +public: + ClickPanel(Panel *parent) + { + _viewIndex = 0; + _textIndex = 0; + SetParent(parent); + AddActionSignalTarget(parent); + + SetCursor(dc_hand); + + SetPaintBackgroundEnabled(false); + SetPaintEnabled(false); +// SetPaintAppearanceEnabled(false); + +#if defined( DRAW_CLICK_PANELS ) + SetPaintEnabled(true); +#endif + } + + void SetTextIndex( int linkStartIndex, int viewStartIndex ) + { + _textIndex = linkStartIndex; + _viewIndex = viewStartIndex; + } + +#if defined( DRAW_CLICK_PANELS ) + virtual void Paint() + { + surface()->DrawSetColor( Color( 255, 0, 0, 255 ) ); + surface()->DrawOutlinedRect( 0, 0, GetWide(), GetTall() ); + } +#endif + + int GetTextIndex() + { + return _textIndex; + } + + int GetViewTextIndex() + { + return _viewIndex; + } + + void OnMousePressed(MouseCode code) + { + if (code == MOUSE_LEFT) + { + PostActionSignal(new KeyValues("ClickPanel", "index", _textIndex)); + } + else + { + GetParent()->OnMousePressed( code ); + } + } + +private: + int _textIndex; + int _viewIndex; +}; + + +//----------------------------------------------------------------------------- +// Purpose: Panel used only to draw the interior border region +//----------------------------------------------------------------------------- +class RichTextInterior : public Panel +{ + DECLARE_CLASS_SIMPLE( RichTextInterior, Panel ); + +public: + RichTextInterior( RichText *pParent, const char *pchName ) : BaseClass( pParent, pchName ) + { + SetKeyBoardInputEnabled( false ); + SetMouseInputEnabled( false ); + SetPaintBackgroundEnabled( false ); + SetPaintEnabled( false ); + m_pRichText = pParent; + } + +/* virtual IAppearance *GetAppearance() + { + if ( m_pRichText->IsScrollbarVisible() ) + return m_pAppearanceScrollbar; + + return BaseClass::GetAppearance(); + }*/ + + virtual void ApplySchemeSettings( IScheme *pScheme ) + { + BaseClass::ApplySchemeSettings( pScheme ); +// m_pAppearanceScrollbar = FindSchemeAppearance( pScheme, "scrollbar_visible" ); + } + +private: + RichText *m_pRichText; +// IAppearance *m_pAppearanceScrollbar; +}; + +}; // namespace vgui + +DECLARE_BUILD_FACTORY( RichText ); + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +RichText::RichText(Panel *parent, const char *panelName) : BaseClass(parent, panelName) +{ + m_bAllTextAlphaIsZero = false; + _font = INVALID_FONT; + m_hFontUnderline = INVALID_FONT; + + m_bRecalcLineBreaks = true; + m_pszInitialText = NULL; + _cursorPos = 0; + _mouseSelection = false; + _mouseDragSelection = false; + _vertScrollBar = new ScrollBar(this, "ScrollBar", true); + _vertScrollBar->AddActionSignalTarget(this); + _recalcSavedRenderState = true; + _maxCharCount = (64 * 1024); + AddActionSignalTarget(this); + m_pInterior = new RichTextInterior( this, NULL ); + + //a -1 for _select[0] means that the selection is empty + _select[0] = -1; + _select[1] = -1; + m_pEditMenu = NULL; + + SetCursor(dc_ibeam); + + //position the cursor so it is at the end of the text + GotoTextEnd(); + + // set default foreground color to black + _defaultTextColor = Color(0, 0, 0, 0); + + // initialize the line break array + InvalidateLineBreakStream(); + + if ( IsProportional() ) + { + int width, height; + int sw,sh; + surface()->GetProportionalBase( width, height ); + surface()->GetScreenSize(sw, sh); + + _drawOffsetX = static_cast<int>( static_cast<float>( DRAW_OFFSET_X )*( static_cast<float>( sw )/ static_cast<float>( width ))); + _drawOffsetY = static_cast<int>( static_cast<float>( DRAW_OFFSET_Y )*( static_cast<float>( sw )/ static_cast<float>( width ))); + } + else + { + _drawOffsetX = DRAW_OFFSET_X; + _drawOffsetY = DRAW_OFFSET_Y; + } + + // add a basic format string + TFormatStream stream; + stream.color = _defaultTextColor; + stream.fade.flFadeStartTime = 0.0f; + stream.fade.flFadeLength = -1.0f; + stream.pixelsIndent = 0; + stream.textStreamIndex = 0; + stream.textClickable = false; + m_FormatStream.AddToTail(stream); + + m_bResetFades = false; + m_bInteractive = true; + m_bUnusedScrollbarInvis = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +RichText::~RichText() +{ + delete [] m_pszInitialText; + delete m_pEditMenu; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::SetDrawOffsets( int ofsx, int ofsy ) +{ + _drawOffsetX = ofsx; + _drawOffsetY = ofsy; +} + +//----------------------------------------------------------------------------- +// Purpose: sets it as drawing text only - used for embedded RichText control into other text drawing situations +//----------------------------------------------------------------------------- +void RichText::SetDrawTextOnly() +{ + SetDrawOffsets( 0, 0 ); + SetPaintBackgroundEnabled( false ); +// SetPaintAppearanceEnabled( false ); + SetPostChildPaintEnabled( false ); + m_pInterior->SetVisible( false ); + SetVerticalScrollbar( false ); +} + +//----------------------------------------------------------------------------- +// Purpose: configures colors +//----------------------------------------------------------------------------- +void RichText::ApplySchemeSettings(IScheme *pScheme) +{ + BaseClass::ApplySchemeSettings(pScheme); + + _font = pScheme->GetFont("Default", IsProportional() ); + m_hFontUnderline = pScheme->GetFont("DefaultUnderline", IsProportional() ); + + SetFgColor(GetSchemeColor("RichText.TextColor", pScheme)); + SetBgColor(GetSchemeColor("RichText.BgColor", pScheme)); + + _selectionTextColor = GetSchemeColor("RichText.SelectedTextColor", GetFgColor(), pScheme); + _selectionColor = GetSchemeColor("RichText.SelectedBgColor", pScheme); + + if ( Q_strlen( pScheme->GetResourceString( "RichText.InsetX" ) ) ) + { + SetDrawOffsets( atoi( pScheme->GetResourceString( "RichText.InsetX" ) ), atoi( pScheme->GetResourceString( "RichText.InsetY" ) ) ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: if the default format color isn't set then set it +//----------------------------------------------------------------------------- +void RichText::SetFgColor( Color color ) +{ + // Replace default format color if + // the stream is empty and the color is the default ( or the previous FgColor ) + if ( m_FormatStream.Size() == 1 && + ( m_FormatStream[0].color == _defaultTextColor || m_FormatStream[0].color == GetFgColor() ) ) + { + m_FormatStream[0].color = color; + } + + BaseClass::SetFgColor( color ); +} + +//----------------------------------------------------------------------------- +// Purpose: Sends a message if the data has changed +// Turns off any selected text in the window if we are not using the edit menu +//----------------------------------------------------------------------------- +void RichText::OnKillFocus() +{ + // check if we clicked the right mouse button or if it is down + bool mouseRightClicked = input()->WasMousePressed(MOUSE_RIGHT); + bool mouseRightUp = input()->WasMouseReleased(MOUSE_RIGHT); + bool mouseRightDown = input()->IsMouseDown(MOUSE_RIGHT); + + if (mouseRightClicked || mouseRightDown || mouseRightUp ) + { + // get the start and ends of the selection area + int start, end; + if (GetSelectedRange(start, end)) // we have selected text + { + // see if we clicked in the selection area + int startX, startY; + CursorToPixelSpace(start, startX, startY); + int endX, endY; + CursorToPixelSpace(end, endX, endY); + int cursorX, cursorY; + input()->GetCursorPos(cursorX, cursorY); + ScreenToLocal(cursorX, cursorY); + + // check the area vertically + // we need to handle the horizontal edge cases eventually + int fontTall = GetLineHeight(); + endY = endY + fontTall; + if ((startY < cursorY) && (endY > cursorY)) + { + // if we clicked in the selection area, leave the text highlighted + return; + } + } + } + + // clear any selection + SelectNone(); + + // chain + BaseClass::OnKillFocus(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Wipe line breaks after the size of a panel has been changed +//----------------------------------------------------------------------------- +void RichText::OnSizeChanged( int wide, int tall ) +{ + BaseClass::OnSizeChanged( wide, tall ); + + // blow away the line breaks list + _invalidateVerticalScrollbarSlider = true; + InvalidateLineBreakStream(); + InvalidateLayout(); + + if ( _vertScrollBar->IsVisible() ) + { + _vertScrollBar->MakeReadyForUse(); + m_pInterior->SetBounds( 0, 0, wide - _vertScrollBar->GetWide(), tall ); + } + else + { + m_pInterior->SetBounds( 0, 0, wide, tall ); + } +} + + +const wchar_t *RichText::ResolveLocalizedTextAndVariables( char const *pchLookup, wchar_t *outbuf, size_t outbufsizeinbytes ) +{ + if ( pchLookup[ 0 ] == '#' ) + { + // try lookup in localization tables + StringIndex_t index = g_pVGuiLocalize->FindIndex( pchLookup + 1 ); + if ( index == INVALID_LOCALIZE_STRING_INDEX ) + { +/* // if it's not found, maybe it's a special expanded variable - look for an expansion + char rgchT[MAX_PATH]; + + // get the variables + KeyValues *variables = GetDialogVariables_R(); + if ( variables ) + { + // see if any are any special vars to put in + for ( KeyValues *pkv = variables->GetFirstSubKey(); pkv != NULL; pkv = pkv->GetNextKey() ) + { + if ( !Q_strncmp( pkv->GetName(), "$", 1 ) ) + { + // make a new lookup, with this key appended + Q_snprintf( rgchT, sizeof( rgchT ), "%s%s=%s", pchLookup, pkv->GetName(), pkv->GetString() ); + index = localize()->FindIndex( rgchT ); + break; + } + } + } + */ + } + + // see if we have a valid string + if ( index != INVALID_LOCALIZE_STRING_INDEX ) + { + wchar_t *format = g_pVGuiLocalize->GetValueByIndex( index ); + Assert( format ); + if ( format ) + { + /*// Try and substitute variables if any + KeyValues *variables = GetDialogVariables_R(); + if ( variables ) + { + localize()->ConstructString( outbuf, outbufsizeinbytes, index, variables ); + return outbuf; + }*/ + } + V_wcsncpy( outbuf, format, outbufsizeinbytes ); + return outbuf; + } + } + + Q_UTF8ToUnicode( pchLookup, outbuf, outbufsizeinbytes ); + return outbuf; +} + +//----------------------------------------------------------------------------- +// Purpose: Set the text array +// Using this function will cause all lineBreaks to be discarded. +// This is because this fxn replaces the contents of the text buffer. +// For modifying large buffers use insert functions. +//----------------------------------------------------------------------------- +void RichText::SetText(const char *text) +{ + if (!text) + { + text = ""; + } + + wchar_t unicode[1024]; + + if (text[0] == '#') + { + ResolveLocalizedTextAndVariables( text, unicode, sizeof( unicode ) ); + SetText( unicode ); + return; + } + + // convert to unicode + Q_UTF8ToUnicode(text, unicode, sizeof(unicode)); + SetText(unicode); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::SetText(const wchar_t *text) +{ + // reset the formatting stream + m_FormatStream.RemoveAll(); + TFormatStream stream; + stream.color = GetFgColor(); + stream.fade.flFadeLength = -1.0f; + stream.fade.flFadeStartTime = 0.0f; + stream.pixelsIndent = 0; + stream.textStreamIndex = 0; + stream.textClickable = false; + m_FormatStream.AddToTail(stream); + + // set the new text stream + m_TextStream.RemoveAll(); + if ( text && *text ) + { + int textLen = wcslen(text) + 1; + m_TextStream.EnsureCapacity(textLen); + for(int i = 0; i < textLen; i++) + { + m_TextStream.AddToTail(text[i]); + } + } + GotoTextStart(); + SelectNone(); + + // blow away the line breaks list + InvalidateLineBreakStream(); + InvalidateLayout(); +} + +//----------------------------------------------------------------------------- +// Purpose: Given cursor's position in the text buffer, convert it to +// the local window's x and y pixel coordinates +// Input: cursorPos: cursor index +// Output: cx, cy, the corresponding coords in the local window +//----------------------------------------------------------------------------- +void RichText::CursorToPixelSpace(int cursorPos, int &cx, int &cy) +{ + int yStart = _drawOffsetY; + int x = _drawOffsetX, y = yStart; + _pixelsIndent = 0; + int lineBreakIndexIndex = 0; + + for (int i = GetStartDrawIndex(lineBreakIndexIndex); i < m_TextStream.Count(); i++) + { + wchar_t ch = m_TextStream[i]; + + // if we've found the position, break + if (cursorPos == i) + { + // if we've passed a line break go to that + if (m_LineBreaks[lineBreakIndexIndex] == i) + { + // add another line + AddAnotherLine(x, y); + lineBreakIndexIndex++; + } + break; + } + + // if we've passed a line break go to that + if (m_LineBreaks[lineBreakIndexIndex] == i) + { + // add another line + AddAnotherLine(x, y); + lineBreakIndexIndex++; + } + + // add to the current position + x += surface()->GetCharacterWidth(_font, ch); + } + + cx = x; + cy = y; +} + +//----------------------------------------------------------------------------- +// Purpose: Converts local pixel coordinates to an index in the text buffer +//----------------------------------------------------------------------------- +int RichText::PixelToCursorSpace(int cx, int cy) +{ + int fontTall = GetLineHeight(); + + // where to start reading + int yStart = _drawOffsetY; + int x = _drawOffsetX, y = yStart; + _pixelsIndent = 0; + int lineBreakIndexIndex = 0; + + int startIndex = GetStartDrawIndex(lineBreakIndexIndex); + if (_recalcSavedRenderState) + { + RecalculateDefaultState(startIndex); + } + + _pixelsIndent = m_CachedRenderState.pixelsIndent; + _currentTextClickable = m_CachedRenderState.textClickable; + TRenderState renderState = m_CachedRenderState; + + bool onRightLine = false; + int i; + for (i = startIndex; i < m_TextStream.Count(); i++) + { + wchar_t ch = m_TextStream[i]; + + renderState.x = x; + if ( UpdateRenderState( i, renderState ) ) + { + x = renderState.x; + } + + // if we are on the right line but off the end of if put the cursor at the end of the line + if (m_LineBreaks[lineBreakIndexIndex] == i) + { + // add another line + AddAnotherLine(x, y); + lineBreakIndexIndex++; + + if (onRightLine) + break; + } + + // check to see if we're on the right line + if (cy < yStart) + { + // cursor is above panel + onRightLine = true; + } + else if (cy >= y && (cy < (y + fontTall + _drawOffsetY))) + { + onRightLine = true; + } + + int wide = surface()->GetCharacterWidth(_font, ch); + + // if we've found the position, break + if (onRightLine) + { + if (cx > GetWide()) // off right side of window + { + } + else if (cx < (_drawOffsetX + renderState.pixelsIndent) || cy < yStart) // off left side of window + { + // Msg( "PixelToCursorSpace() off left size, returning %d '%c'\n", i, m_TextStream[i] ); + return i; // move cursor one to left + } + + if (cx >= x && cx < (x + wide)) + { + // check which side of the letter they're on + if (cx < (x + (wide * 0.5))) // left side + { + // Msg( "PixelToCursorSpace() on the left size, returning %d '%c'\n", i, m_TextStream[i] ); + return i; + } + else // right side + { + // Msg( "PixelToCursorSpace() on the right size, returning %d '%c'\n", i + 1, m_TextStream[i + 1] ); + return i + 1; + } + } + } + x += wide; + } + + // Msg( "PixelToCursorSpace() never hit, returning %d\n", i ); + return i; +} + +//----------------------------------------------------------------------------- +// Purpose: Draws a string of characters in the panel +// Input: iFirst - Index of the first character to draw +// iLast - Index of the last character to draw +// renderState - Render state to use +// font- font to use +// Output: returns the width of the character drawn +//----------------------------------------------------------------------------- +int RichText::DrawString(int iFirst, int iLast, TRenderState &renderState, HFont font) +{ +// VPROF( "RichText::DrawString" ); + + // Calculate the render size + int fontTall = surface()->GetFontTall(font); + // BUGBUG John: This won't exactly match the rendered size + int charWide = 0; + for ( int i = iFirst; i <= iLast; i++ ) + { + wchar_t ch = m_TextStream[i]; +#if USE_GETKERNEDCHARWIDTH + wchar_t chBefore = 0; + wchar_t chAfter = 0; + if ( i > 0 ) + chBefore = m_TextStream[i-1]; + if ( i < iLast ) + chAfter = m_TextStream[i+1]; + float flWide = 0.0f, flabcA = 0.0f; + surface()->GetKernedCharWidth(font, ch, chBefore, chAfter, flWide, flabcA); + if ( ch == L' ' ) + flWide = ceil( flWide ); + charWide += floor( flWide + 0.6 ); +#else + charWide += surface()->GetCharacterWidth(font, ch); +#endif + } + + // draw selection, if any + int selection0 = -1, selection1 = -1; + GetSelectedRange(selection0, selection1); + + if (iFirst >= selection0 && iFirst < selection1) + { + // draw background selection color + surface()->DrawSetColor(_selectionColor); + surface()->DrawFilledRect(renderState.x, renderState.y, renderState.x + charWide, renderState.y + 1 + fontTall); + + // reset text color + surface()->DrawSetTextColor(_selectionTextColor); + m_bAllTextAlphaIsZero = false; + } + else + { + surface()->DrawSetTextColor(renderState.textColor); + } + + if ( renderState.textColor.a() != 0 ) + { + m_bAllTextAlphaIsZero = false; + surface()->DrawSetTextPos(renderState.x, renderState.y); + surface()->DrawPrintText(&m_TextStream[iFirst], iLast - iFirst + 1); + } + + return charWide; +} + +//----------------------------------------------------------------------------- +// Purpose: Finish drawing url +//----------------------------------------------------------------------------- +void RichText::FinishingURL(int x, int y) +{ + // finishing URL + if ( _clickableTextPanels.IsValidIndex( _clickableTextIndex ) ) + { + ClickPanel *clickPanel = _clickableTextPanels[ _clickableTextIndex ]; + int px, py; + clickPanel->GetPos(px, py); + int fontTall = GetLineHeight(); + clickPanel->SetSize( MAX( x - px, 6 ), y - py + fontTall ); + clickPanel->SetVisible(true); + + // if we haven't actually advanced any, step back and ignore this one + // this is probably a data input problem though, need to find root cause + if ( x - px <= 0 ) + { + --_clickableTextIndex; + clickPanel->SetVisible(false); + } + } +} + +void RichText::CalculateFade( TRenderState &renderState ) +{ + if ( m_FormatStream.IsValidIndex( renderState.formatStreamIndex ) ) + { + if ( m_bResetFades == false ) + { + if ( m_FormatStream[renderState.formatStreamIndex].fade.flFadeLength != -1.0f ) + { + float frac = ( m_FormatStream[renderState.formatStreamIndex].fade.flFadeStartTime - system()->GetCurrentTime() ) / m_FormatStream[renderState.formatStreamIndex].fade.flFadeLength; + + int alpha = frac * m_FormatStream[renderState.formatStreamIndex].fade.iOriginalAlpha; + alpha = clamp( alpha, 0, m_FormatStream[renderState.formatStreamIndex].fade.iOriginalAlpha ); + + renderState.textColor.SetColor( renderState.textColor.r(), renderState.textColor.g(), renderState.textColor.b(), alpha ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Draws the text in the panel +//----------------------------------------------------------------------------- +void RichText::Paint() +{ + // Assume the worst + m_bAllTextAlphaIsZero = true; + + HFont hFontCurrent = _font; + + // hide all the clickable panels until we know where they are to reside + for (int j = 0; j < _clickableTextPanels.Count(); j++) + { + _clickableTextPanels[j]->SetVisible(false); + } + + if ( !HasText() ) + return; + + int wide, tall; + GetSize( wide, tall ); + + int lineBreakIndexIndex = 0; + int startIndex = GetStartDrawIndex(lineBreakIndexIndex); + _currentTextClickable = false; + + _clickableTextIndex = GetClickableTextIndexStart(startIndex); + + // recalculate and cache the render state at the render start + if (_recalcSavedRenderState) + { + RecalculateDefaultState(startIndex); + } + // copy off the cached render state + TRenderState renderState = m_CachedRenderState; + + _pixelsIndent = m_CachedRenderState.pixelsIndent; + _currentTextClickable = m_CachedRenderState.textClickable; + + renderState.textClickable = _currentTextClickable; + renderState.textColor = m_FormatStream[renderState.formatStreamIndex].color; + CalculateFade( renderState ); + + renderState.formatStreamIndex++; + + if ( _currentTextClickable ) + { + _clickableTextIndex = startIndex; + } + + // where to start drawing + renderState.x = _drawOffsetX + _pixelsIndent; + renderState.y = _drawOffsetY; + + // draw the text + int selection0 = -1, selection1 = -1; + GetSelectedRange(selection0, selection1); + + surface()->DrawSetTextFont( hFontCurrent ); + + for (int i = startIndex; i < m_TextStream.Count() && renderState.y < tall; ) + { + // 1. + // Update our current render state based on the formatting and color streams, + // this has to happen if it's our very first iteration, or if we are actually changing + // state. + int nXBeforeStateChange = renderState.x; + if ( UpdateRenderState(i, renderState) || i == startIndex ) + { + // check for url state change + if (renderState.textClickable != _currentTextClickable) + { + if (renderState.textClickable) + { + // entering new URL + _clickableTextIndex++; + hFontCurrent = m_hFontUnderline; + surface()->DrawSetTextFont( hFontCurrent ); + + // set up the panel + ClickPanel *clickPanel = _clickableTextPanels.IsValidIndex( _clickableTextIndex ) ? _clickableTextPanels[_clickableTextIndex] : NULL; + + if (clickPanel) + { + clickPanel->SetPos(renderState.x, renderState.y); + } + } + else + { + FinishingURL(nXBeforeStateChange, renderState.y); + hFontCurrent = _font; + surface()->DrawSetTextFont( hFontCurrent ); + } + _currentTextClickable = renderState.textClickable; + } + } + + // 2. + // if we've passed a line break go to that + if ( m_LineBreaks.IsValidIndex( lineBreakIndexIndex ) && m_LineBreaks[lineBreakIndexIndex] <= i ) + { + if (_currentTextClickable) + { + FinishingURL(renderState.x, renderState.y); + } + + // add another line + AddAnotherLine(renderState.x, renderState.y); + lineBreakIndexIndex++; + + // Skip white space unless the previous line ended from the hard carriage return + if ( i && ( m_TextStream[i-1] != '\n' ) && ( m_TextStream[i-1] != '\r') ) + { + while ( m_TextStream[i] == L' ' ) + { + if ( i+1 < m_TextStream.Count() ) + ++i; + else + break; + } + } + + if (renderState.textClickable) + { + // move to the next URL + _clickableTextIndex++; + ClickPanel *clickPanel = _clickableTextPanels.IsValidIndex( _clickableTextIndex ) ? _clickableTextPanels[_clickableTextIndex] : NULL; + if (clickPanel) + { + clickPanel->SetPos(renderState.x, renderState.y); + } + } + } + + // 3. + // Calculate the range of text to draw all at once + int iLim = m_TextStream.Count(); + + // Stop at the next format change + if ( m_FormatStream.IsValidIndex(renderState.formatStreamIndex) && + m_FormatStream[renderState.formatStreamIndex].textStreamIndex < iLim && + m_FormatStream[renderState.formatStreamIndex].textStreamIndex >= i && + m_FormatStream[renderState.formatStreamIndex].textStreamIndex ) + { + iLim = m_FormatStream[renderState.formatStreamIndex].textStreamIndex; + } + + // Stop at the next line break + if ( m_LineBreaks.IsValidIndex( lineBreakIndexIndex ) && m_LineBreaks[lineBreakIndexIndex] < iLim ) + iLim = m_LineBreaks[lineBreakIndexIndex]; + + // Handle non-drawing characters specially + for ( int iT = i; iT < iLim; iT++ ) + { + if ( iswcntrl(m_TextStream[iT]) ) + { + iLim = iT; + break; + } + } + + // 4. + // Draw the current text range + if ( iLim <= i ) + { + if ( m_TextStream[i] == '\t' ) + { + int dxTabWidth = 8 * surface()->GetCharacterWidth(hFontCurrent, ' '); + dxTabWidth = MAX( 1, dxTabWidth ); + + renderState.x = ( dxTabWidth * ( 1 + ( renderState.x / dxTabWidth ) ) ); + } + i++; + } + else + { + renderState.x += DrawString(i, iLim - 1, renderState, hFontCurrent ); + i = iLim; + } + } + + if (renderState.textClickable) + { + FinishingURL(renderState.x, renderState.y); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +int RichText::GetClickableTextIndexStart(int startIndex) +{ + // cycle to the right url panel for what is visible after the startIndex. + for (int i = 0; i < _clickableTextPanels.Count(); i++) + { + if (_clickableTextPanels[i]->GetViewTextIndex() >= startIndex) + { + return i - 1; + } + } + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Recalcultes the formatting state from the specified index +//----------------------------------------------------------------------------- +void RichText::RecalculateDefaultState(int startIndex) +{ + if (!HasText() ) + return; + + Assert(startIndex < m_TextStream.Count()); + + m_CachedRenderState.textColor = GetFgColor(); + _pixelsIndent = 0; + _currentTextClickable = false; + _clickableTextIndex = GetClickableTextIndexStart(startIndex); + + // find where in the formatting stream we need to be + GenerateRenderStateForTextStreamIndex(startIndex, m_CachedRenderState); + _recalcSavedRenderState = false; +} + +//----------------------------------------------------------------------------- +// Purpose: updates a render state based on the formatting and color streams +// Output: true if we changed the render state +//----------------------------------------------------------------------------- +bool RichText::UpdateRenderState(int textStreamPos, TRenderState &renderState) +{ + // check the color stream + if (m_FormatStream.IsValidIndex(renderState.formatStreamIndex) && + m_FormatStream[renderState.formatStreamIndex].textStreamIndex == textStreamPos) + { + // set the current formatting + renderState.textColor = m_FormatStream[renderState.formatStreamIndex].color; + renderState.textClickable = m_FormatStream[renderState.formatStreamIndex].textClickable; + + CalculateFade( renderState ); + + int indentChange = m_FormatStream[renderState.formatStreamIndex].pixelsIndent - renderState.pixelsIndent; + renderState.pixelsIndent = m_FormatStream[renderState.formatStreamIndex].pixelsIndent; + + if (indentChange) + { + renderState.x = renderState.pixelsIndent + _drawOffsetX; + } + + //!! for supporting old functionality, store off state in globals + _pixelsIndent = renderState.pixelsIndent; + + // move to the next position in the color stream + renderState.formatStreamIndex++; + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the index in the format stream for the specified text stream index +//----------------------------------------------------------------------------- +int RichText::FindFormatStreamIndexForTextStreamPos(int textStreamIndex) +{ + int formatStreamIndex = 0; + for (; m_FormatStream.IsValidIndex(formatStreamIndex); formatStreamIndex++) + { + if (m_FormatStream[formatStreamIndex].textStreamIndex > textStreamIndex) + break; + } + + // step back to the color change before the new line + formatStreamIndex--; + if (!m_FormatStream.IsValidIndex(formatStreamIndex)) + { + formatStreamIndex = 0; + } + return formatStreamIndex; +} + +//----------------------------------------------------------------------------- +// Purpose: Generates a base renderstate given a index into the text stream +//----------------------------------------------------------------------------- +void RichText::GenerateRenderStateForTextStreamIndex(int textStreamIndex, TRenderState &renderState) +{ + // find where in the format stream we need to be given the specified place in the text stream + renderState.formatStreamIndex = FindFormatStreamIndexForTextStreamPos(textStreamIndex); + + // copy the state data + renderState.textColor = m_FormatStream[renderState.formatStreamIndex].color; + renderState.pixelsIndent = m_FormatStream[renderState.formatStreamIndex].pixelsIndent; + renderState.textClickable = m_FormatStream[renderState.formatStreamIndex].textClickable; +} + +//----------------------------------------------------------------------------- +// Purpose: Called pre render +//----------------------------------------------------------------------------- +void RichText::OnThink() +{ + if (m_bRecalcLineBreaks) + { + _recalcSavedRenderState = true; + RecalculateLineBreaks(); + + // recalculate scrollbar position + if (_invalidateVerticalScrollbarSlider) + { + LayoutVerticalScrollBarSlider(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Called when data changes or panel size changes +//----------------------------------------------------------------------------- +void RichText::PerformLayout() +{ + BaseClass::PerformLayout(); + + // force a Repaint + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: inserts a color change into the formatting stream +//----------------------------------------------------------------------------- +void RichText::InsertColorChange(Color col) +{ + // see if color already exists in text stream + TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1]; + if (prevItem.color == col) + { + // inserting same color into stream, just ignore + } + else if (prevItem.textStreamIndex == m_TextStream.Count()) + { + // this item is in the same place; update values + prevItem.color = col; + } + else + { + // add to text stream, based off existing item + TFormatStream streamItem = prevItem; + streamItem.color = col; + streamItem.textStreamIndex = m_TextStream.Count(); + m_FormatStream.AddToTail(streamItem); + } +} + +//----------------------------------------------------------------------------- +// Purpose: inserts a fade into the formatting stream +//----------------------------------------------------------------------------- +void RichText::InsertFade( float flSustain, float flLength ) +{ + // see if color already exists in text stream + TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1]; + if (prevItem.textStreamIndex == m_TextStream.Count()) + { + // this item is in the same place; update values + prevItem.fade.flFadeStartTime = system()->GetCurrentTime() + flSustain; + prevItem.fade.flFadeSustain = flSustain; + prevItem.fade.flFadeLength = flLength; + prevItem.fade.iOriginalAlpha = prevItem.color.a(); + } + else + { + // add to text stream, based off existing item + TFormatStream streamItem = prevItem; + + prevItem.fade.flFadeStartTime = system()->GetCurrentTime() + flSustain; + prevItem.fade.flFadeLength = flLength; + prevItem.fade.flFadeSustain = flSustain; + prevItem.fade.iOriginalAlpha = prevItem.color.a(); + + streamItem.textStreamIndex = m_TextStream.Count(); + m_FormatStream.AddToTail(streamItem); + } +} + +void RichText::ResetAllFades( bool bHold, bool bOnlyExpired, float flNewSustain ) +{ + m_bResetFades = bHold; + + if ( m_bResetFades == false ) + { + for (int i = 1; i < m_FormatStream.Count(); i++) + { + if ( bOnlyExpired == true ) + { + if ( m_FormatStream[i].fade.flFadeStartTime >= system()->GetCurrentTime() ) + continue; + } + + if ( flNewSustain == -1.0f ) + { + flNewSustain = m_FormatStream[i].fade.flFadeSustain; + } + + m_FormatStream[i].fade.flFadeStartTime = system()->GetCurrentTime() + flNewSustain; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: inserts an indent change into the formatting stream +//----------------------------------------------------------------------------- +void RichText::InsertIndentChange(int pixelsIndent) +{ + if (pixelsIndent < 0) + { + pixelsIndent = 0; + } + else if (pixelsIndent > 255) + { + pixelsIndent = 255; + } + + // see if indent change already exists in text stream + TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1]; + if (prevItem.pixelsIndent == pixelsIndent) + { + // inserting same indent into stream, just ignore + } + else if (prevItem.textStreamIndex == m_TextStream.Count()) + { + // this item is in the same place; update + prevItem.pixelsIndent = pixelsIndent; + } + else + { + // add to text stream, based off existing item + TFormatStream streamItem = prevItem; + streamItem.pixelsIndent = pixelsIndent; + streamItem.textStreamIndex = m_TextStream.Count(); + m_FormatStream.AddToTail(streamItem); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Inserts character Start for clickable text, eg. URLS +//----------------------------------------------------------------------------- +void RichText::InsertClickableTextStart( const char *pchClickAction ) +{ + // see if indent change already exists in text stream + TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1]; + TFormatStream *pFormatStream = &prevItem; + if (prevItem.textStreamIndex == m_TextStream.Count()) + { + // this item is in the same place; update + prevItem.textClickable = true; + pFormatStream->m_sClickableTextAction = pchClickAction; + } + else + { + // add to text stream, based off existing item + TFormatStream formatStreamCopy = prevItem; + int iFormatStream = m_FormatStream.AddToTail( formatStreamCopy ); + + // set the new params + pFormatStream = &m_FormatStream[iFormatStream]; + pFormatStream->textStreamIndex = m_TextStream.Count(); + pFormatStream->textClickable = true; + pFormatStream->m_sClickableTextAction = pchClickAction; + } + + // invalidate the layout to recalculate where the click panels should go + InvalidateLineBreakStream(); + InvalidateLayout(); +} + +//----------------------------------------------------------------------------- +// Purpose: Inserts character end for clickable text, eg. URLS +//----------------------------------------------------------------------------- +void RichText::InsertClickableTextEnd() +{ + // see if indent change already exists in text stream + TFormatStream &prevItem = m_FormatStream[m_FormatStream.Count() - 1]; + if (!prevItem.textClickable) + { + // inserting same indent into stream, just ignore + } + else if (prevItem.textStreamIndex == m_TextStream.Count()) + { + // this item is in the same place; update + prevItem.textClickable = false; + } + else + { + // add to text stream, based off existing item + TFormatStream streamItem = prevItem; + streamItem.textClickable = false; + streamItem.textStreamIndex = m_TextStream.Count(); + m_FormatStream.AddToTail(streamItem); + } +} + +//----------------------------------------------------------------------------- +// Purpose: moves x,y to the Start of the next line of text +//----------------------------------------------------------------------------- +void RichText::AddAnotherLine(int &cx, int &cy) +{ + cx = _drawOffsetX + _pixelsIndent; + cy += (GetLineHeight() + _drawOffsetY); +} + +//----------------------------------------------------------------------------- +// Purpose: Recalculates line breaks +//----------------------------------------------------------------------------- +void RichText::RecalculateLineBreaks() +{ + if ( !m_bRecalcLineBreaks ) + return; + + int wide = GetWide(); + if (!wide) + return; + + wide -= _drawOffsetX; + + m_bRecalcLineBreaks = false; + _recalcSavedRenderState = true; + if (!HasText()) + return; + + int selection0 = -1, selection1 = -1; + + // subtract the scrollbar width + if (_vertScrollBar->IsVisible()) + { + wide -= _vertScrollBar->GetWide(); + } + + int x = _drawOffsetX, y = _drawOffsetY; + + HFont fontWordStart = INVALID_FONT; + int wordStartIndex = 0; + int lineStartIndex = 0; + bool hasWord = false; + bool justStartedNewLine = true; + bool wordStartedOnNewLine = true; + + int startChar = 0; + if (_recalculateBreaksIndex <= 0) + { + m_LineBreaks.RemoveAll(); + } + else + { + // remove the rest of the linebreaks list since its out of date. + for (int i = _recalculateBreaksIndex + 1; i < m_LineBreaks.Count(); ++i) + { + m_LineBreaks.Remove(i); + --i; // removing shrinks the list! + } + startChar = m_LineBreaks[_recalculateBreaksIndex]; + lineStartIndex = m_LineBreaks[_recalculateBreaksIndex]; + wordStartIndex = lineStartIndex; + } + + // handle the case where this char is a new line, in that case + // we have already taken its break index into account above so skip it. + if (m_TextStream[startChar] == '\r' || m_TextStream[startChar] == '\n') + { + startChar++; + lineStartIndex = startChar; + } + + // cycle to the right url panel for what is visible after the startIndex. + int clickableTextNum = GetClickableTextIndexStart(startChar); + clickableTextNum++; + + // initialize the renderstate with the start + TRenderState renderState; + GenerateRenderStateForTextStreamIndex(startChar, renderState); + _currentTextClickable = false; + + HFont font = _font; + + bool bForceBreak = false; + float flLineWidthSoFar = 0; + + // loop through all the characters + for (int i = startChar; i < m_TextStream.Count(); ++i) + { + wchar_t ch = m_TextStream[i]; + renderState.x = x; + if (UpdateRenderState(i, renderState)) + { + x = renderState.x; + int preI = i; + + // check for clickable text + if (renderState.textClickable != _currentTextClickable) + { + if (renderState.textClickable) + { + // make a new clickable text panel + if (clickableTextNum >= _clickableTextPanels.Count()) + { + _clickableTextPanels.AddToTail(new ClickPanel(this)); + } + + ClickPanel *clickPanel = _clickableTextPanels[clickableTextNum++]; + clickPanel->SetTextIndex(preI, preI); + } + + // url state change + _currentTextClickable = renderState.textClickable; + } + } + + bool bIsWSpace = iswspace( ch ) ? true : false; + + bool bPreviousWordStartedOnNewLine = wordStartedOnNewLine; + int iPreviousWordStartIndex = wordStartIndex; + if ( !bIsWSpace && ch != L'\t' && ch != L'\n' && ch != L'\r' ) + { + if (!hasWord) + { + // Start a new word + wordStartIndex = i; + hasWord = true; + wordStartedOnNewLine = justStartedNewLine; + fontWordStart = font; + } + // else append to the current word + } + else + { + // whitespace/punctuation character + // end the word + hasWord = false; + } + + float w = 0; + wchar_t wchBefore = 0; + wchar_t wchAfter = 0; + + if ( i > 0 && i > lineStartIndex && i != selection0 && i-1 != selection1 ) + wchBefore = m_TextStream[i-1]; + if ( i < m_TextStream.Count() - 1 && i+1 != selection0 && i != selection1 ) + wchAfter = m_TextStream[i+1]; + + float flabcA; + surface()->GetKernedCharWidth( font, ch, wchBefore, wchAfter, w, flabcA ); + flLineWidthSoFar += w; + + // See if we've exceeded the width we have available, with + if ( floor(flLineWidthSoFar + 0.6) + x > wide ) + { + bForceBreak = true; + } + + if (!iswcntrl(ch)) + { + justStartedNewLine = false; + } + + if ( bForceBreak || ch == '\r' || ch == '\n' ) + { + bForceBreak = false; + // add another line + AddAnotherLine(x, y); + + if ( ch == '\r' || ch == '\n' ) + { + // skip the newline so it's not at the beginning of the new line + lineStartIndex = i + 1; + m_LineBreaks.AddToTail(i + 1); + } + else if ( bPreviousWordStartedOnNewLine || iPreviousWordStartIndex <= lineStartIndex ) + { + lineStartIndex = i; + m_LineBreaks.AddToTail( i ); + + if (renderState.textClickable) + { + // need to split the url into two panels + int oldIndex = _clickableTextPanels[clickableTextNum - 1]->GetTextIndex(); + + // make a new clickable text panel + if (clickableTextNum >= _clickableTextPanels.Count()) + { + _clickableTextPanels.AddToTail(new ClickPanel(this)); + } + + ClickPanel *clickPanel = _clickableTextPanels[clickableTextNum++]; + clickPanel->SetTextIndex(oldIndex, i); + } + } + else + { + m_LineBreaks.AddToTail( iPreviousWordStartIndex ); + lineStartIndex = iPreviousWordStartIndex; + i = iPreviousWordStartIndex; + + TRenderState renderStateAtLastWord; + GenerateRenderStateForTextStreamIndex( i, renderStateAtLastWord ); + + // If the word is clickable, and that started prior to the beginning of the word, then we must split the click panel + if ( renderStateAtLastWord.textClickable && m_FormatStream[ renderStateAtLastWord.formatStreamIndex ].textStreamIndex < i ) + { + // need to split the url into two panels + int oldIndex = _clickableTextPanels[clickableTextNum - 1]->GetTextIndex(); + + // make a new clickable text panel + if (clickableTextNum >= _clickableTextPanels.Count()) + { + _clickableTextPanels.AddToTail(new ClickPanel(this)); + } + + ClickPanel *clickPanel = _clickableTextPanels[clickableTextNum++]; + clickPanel->SetTextIndex(oldIndex, i); + } + } + + flLineWidthSoFar = 0; + justStartedNewLine = true; + hasWord = false; + wordStartedOnNewLine = false; + _currentTextClickable = false; + continue; + } + } + + // end the list + m_LineBreaks.AddToTail(MAX_BUFFER_SIZE); + + // set up the scrollbar + _invalidateVerticalScrollbarSlider = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Recalculate where the vertical scroll bar slider should be +// based on the current cursor line we are on. +//----------------------------------------------------------------------------- +void RichText::LayoutVerticalScrollBarSlider() +{ + _invalidateVerticalScrollbarSlider = false; + + // set up the scrollbar + //if (!_vertScrollBar->IsVisible()) + // return; + + + // see where the scrollbar currently is + int previousValue = _vertScrollBar->GetValue(); + bool bCurrentlyAtEnd = false; + int rmin, rmax; + _vertScrollBar->GetRange(rmin, rmax); + if (rmax && (previousValue + rmin + _vertScrollBar->GetRangeWindow() == rmax)) + { + bCurrentlyAtEnd = true; + } + + // work out position to put scrollbar, factoring in insets + int wide, tall; + GetSize( wide, tall ); + + _vertScrollBar->SetPos( wide - _vertScrollBar->GetWide(), 0 ); + // scrollbar is inside the borders. + _vertScrollBar->SetSize( _vertScrollBar->GetWide(), tall ); + + // calculate how many lines we can fully display + int displayLines = tall / (GetLineHeight() + _drawOffsetY); + int numLines = m_LineBreaks.Count(); + + if (numLines <= displayLines) + { + // disable the scrollbar + _vertScrollBar->SetEnabled(false); + _vertScrollBar->SetRange(0, numLines); + _vertScrollBar->SetRangeWindow(numLines); + _vertScrollBar->SetValue(0); + + if ( m_bUnusedScrollbarInvis ) + { + SetVerticalScrollbar( false ); + } + } + else + { + if ( m_bUnusedScrollbarInvis ) + { + SetVerticalScrollbar( true ); + } + + // set the scrollbars range + _vertScrollBar->SetRange(0, numLines); + _vertScrollBar->SetRangeWindow(displayLines); + _vertScrollBar->SetEnabled(true); + + // this should make it scroll one line at a time + _vertScrollBar->SetButtonPressedScrollValue(1); + if (bCurrentlyAtEnd) + { + _vertScrollBar->SetValue(numLines - displayLines); + } + _vertScrollBar->InvalidateLayout(); + _vertScrollBar->Repaint(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets whether a vertical scrollbar is visible +//----------------------------------------------------------------------------- +void RichText::SetVerticalScrollbar(bool state) +{ + if (_vertScrollBar->IsVisible() != state) + { + _vertScrollBar->SetVisible(state); + InvalidateLineBreakStream(); + InvalidateLayout(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Create cut/copy/paste dropdown menu +//----------------------------------------------------------------------------- +void RichText::CreateEditMenu() +{ + // create a drop down cut/copy/paste menu appropriate for this object's states + if (m_pEditMenu) + delete m_pEditMenu; + m_pEditMenu = new Menu(this, "EditMenu"); + + + // add cut/copy/paste drop down options if its editable, just copy if it is not + m_pEditMenu->AddMenuItem("C&opy", new KeyValues("DoCopySelected"), this); + + m_pEditMenu->SetVisible(false); + m_pEditMenu->SetParent(this); + m_pEditMenu->AddActionSignalTarget(this); +} + +//----------------------------------------------------------------------------- +// Purpose: We want single line windows to scroll horizontally and select text +// in response to clicking and holding outside window +//----------------------------------------------------------------------------- +void RichText::OnMouseFocusTicked() +{ + // if a button is down move the scrollbar slider the appropriate direction + if (_mouseDragSelection) // text is being selected via mouse clicking and dragging + { + OnCursorMoved(0,0); // we want the text to scroll as if we were dragging + } +} + +//----------------------------------------------------------------------------- +// Purpose: If a cursor enters the window, we are not elegible for +// MouseFocusTicked events +//----------------------------------------------------------------------------- +void RichText::OnCursorEntered() +{ + _mouseDragSelection = false; // outside of window dont recieve drag scrolling ticks +} + +//----------------------------------------------------------------------------- +// Purpose: When the cursor is outside the window, if we are holding the mouse +// button down, then we want the window to scroll the text one char at a time using Ticks +//----------------------------------------------------------------------------- +void RichText::OnCursorExited() +{ + // outside of window recieve drag scrolling ticks + if (_mouseSelection) + { + _mouseDragSelection = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle selection of text by mouse +//----------------------------------------------------------------------------- +void RichText::OnCursorMoved(int x, int y) +{ + if (_mouseSelection) + { + // update the cursor position + int x, y; + input()->GetCursorPos(x, y); + ScreenToLocal(x, y); + _cursorPos = PixelToCursorSpace(x, y); + + if (_cursorPos != _select[1]) + { + _select[1] = _cursorPos; + Repaint(); + } + // Msg( "selecting range [%d..%d]\n", _select[0], _select[1] ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle mouse button down events. +//----------------------------------------------------------------------------- +void RichText::OnMousePressed(MouseCode code) +{ + if (code == MOUSE_LEFT) + { + // clear current selection + SelectNone(); + + // move the cursor to where the mouse was pressed + int x, y; + input()->GetCursorPos(x, y); + ScreenToLocal(x, y); + + _cursorPos = PixelToCursorSpace(x, y); + + if ( m_bInteractive ) + { + // enter selection mode + input()->SetMouseCapture(GetVPanel()); + _mouseSelection = true; + + if (_select[0] < 0) + { + // if no initial selection position, Start selection position at cursor + _select[0] = _cursorPos; + } + _select[1] = _cursorPos; + } + + RequestFocus(); + Repaint(); + } + else if (code == MOUSE_RIGHT) // check for context menu open + { + if ( m_bInteractive ) + { + CreateEditMenu(); + Assert(m_pEditMenu); + + OpenEditMenu(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle mouse button up events +//----------------------------------------------------------------------------- +void RichText::OnMouseReleased(MouseCode code) +{ + _mouseSelection = false; + input()->SetMouseCapture(NULL); + + // make sure something has been selected + int cx0, cx1; + if (GetSelectedRange(cx0, cx1)) + { + if (cx1 - cx0 == 0) + { + // nullify selection + _select[0] = -1; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Handle mouse double clicks +//----------------------------------------------------------------------------- +void RichText::OnMouseDoublePressed(MouseCode code) +{ + if ( !m_bInteractive ) + return; + + // left double clicking on a word selects the word + if (code == MOUSE_LEFT) + { + // move the cursor just as if you single clicked. + OnMousePressed(code); + // then find the start and end of the word we are in to highlight it. + int selectSpot[2]; + GotoWordLeft(); + selectSpot[0] = _cursorPos; + GotoWordRight(); + selectSpot[1] = _cursorPos; + + if ( _cursorPos > 0 && (_cursorPos-1) < m_TextStream.Count() ) + { + if (iswspace(m_TextStream[_cursorPos-1])) + { + selectSpot[1]--; + _cursorPos--; + } + } + + _select[0] = selectSpot[0]; + _select[1] = selectSpot[1]; + _mouseSelection = true; + } + +} + +//----------------------------------------------------------------------------- +// Purpose: Turn off text selection code when mouse button is not down +//----------------------------------------------------------------------------- +void RichText::OnMouseCaptureLost() +{ + _mouseSelection = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Masks which keys get chained up +// Maps keyboard input to text window functions. +//----------------------------------------------------------------------------- +void RichText::OnKeyCodeTyped(KeyCode code) +{ + bool shift = (input()->IsKeyDown(KEY_LSHIFT) || input()->IsKeyDown(KEY_RSHIFT)); + bool ctrl = (input()->IsKeyDown(KEY_LCONTROL) || input()->IsKeyDown(KEY_RCONTROL)); + bool alt = (input()->IsKeyDown(KEY_LALT) || input()->IsKeyDown(KEY_RALT)); + bool winkey = (input()->IsKeyDown(KEY_LWIN) || input()->IsKeyDown(KEY_RWIN)); + bool fallThrough = false; + + if ( ctrl || ( winkey && IsOSX() ) ) + { + switch(code) + { + case KEY_INSERT: + case KEY_C: + case KEY_X: + { + CopySelected(); + break; + } + case KEY_PAGEUP: + case KEY_HOME: + { + GotoTextStart(); + break; + } + case KEY_PAGEDOWN: + case KEY_END: + { + GotoTextEnd(); + break; + } + default: + { + fallThrough = true; + break; + } + } + } + else if (alt) + { + // do nothing with ALT-x keys + fallThrough = true; + } + else + { + switch(code) + { + case KEY_TAB: + case KEY_LSHIFT: + case KEY_RSHIFT: + case KEY_ESCAPE: + case KEY_ENTER: + { + fallThrough = true; + break; + } + case KEY_DELETE: + { + if (shift) + { + // shift-delete is cut + CopySelected(); + } + break; + } + case KEY_HOME: + { + GotoTextStart(); + break; + } + case KEY_END: + { + GotoTextEnd(); + break; + } + case KEY_PAGEUP: + { + // if there is a scroll bar scroll down one rangewindow + if (_vertScrollBar->IsVisible()) + { + int window = _vertScrollBar->GetRangeWindow(); + int newval = _vertScrollBar->GetValue(); + _vertScrollBar->SetValue(newval - window - 1); + } + break; + + } + case KEY_PAGEDOWN: + { + // if there is a scroll bar scroll down one rangewindow + if (_vertScrollBar->IsVisible()) + { + int window = _vertScrollBar->GetRangeWindow(); + int newval = _vertScrollBar->GetValue(); + _vertScrollBar->SetValue(newval + window + 1); + } + break; + } + default: + { + // return if any other char is pressed. + // as it will be a unicode char. + // and we don't want select[1] changed unless a char was pressed that this fxn handles + return; + } + } + } + + // select[1] is the location in the line where the blinking cursor started + _select[1] = _cursorPos; + + // chain back on some keys + if (fallThrough) + { + BaseClass::OnKeyCodeTyped(code); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Scrolls the list according to the mouse wheel movement +//----------------------------------------------------------------------------- +void RichText::OnMouseWheeled(int delta) +{ + MoveScrollBar(delta); +} + +//----------------------------------------------------------------------------- +// Purpose: Scrolls the list +// Input : delta - amount to move scrollbar up +//----------------------------------------------------------------------------- +void RichText::MoveScrollBar(int delta) +{ + MoveScrollBarDirect( delta * 3 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Scrolls the list +// Input : delta - amount to move scrollbar up +//----------------------------------------------------------------------------- +void RichText::MoveScrollBarDirect(int delta) +{ + if (_vertScrollBar->IsVisible()) + { + int val = _vertScrollBar->GetValue(); + val -= delta; + _vertScrollBar->SetValue(val); + _recalcSavedRenderState = true; + } +} + +//----------------------------------------------------------------------------- +// Purpose: set the maximum number of chars in the text buffer +//----------------------------------------------------------------------------- +void RichText::SetMaximumCharCount(int maxChars) +{ + _maxCharCount = maxChars; +} + +//----------------------------------------------------------------------------- +// Purpose: Find out what line the cursor is on +//----------------------------------------------------------------------------- +int RichText::GetCursorLine() +{ + // always returns the last place + int pos = m_LineBreaks[m_LineBreaks.Count() - 1]; + Assert(pos == MAX_BUFFER_SIZE); + return pos; +} + +//----------------------------------------------------------------------------- +// Purpose: Move the cursor over to the Start of the next word to the right +//----------------------------------------------------------------------------- +void RichText::GotoWordRight() +{ + // search right until we hit a whitespace character or a newline + while (++_cursorPos < m_TextStream.Count()) + { + if (iswspace(m_TextStream[_cursorPos])) + break; + } + + // search right until we hit an nonspace character + while (++_cursorPos < m_TextStream.Count()) + { + if (!iswspace(m_TextStream[_cursorPos])) + break; + } + + if (_cursorPos > m_TextStream.Count()) + { + _cursorPos = m_TextStream.Count(); + } + + // now we are at the start of the next word + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Move the cursor over to the Start of the next word to the left +//----------------------------------------------------------------------------- +void RichText::GotoWordLeft() +{ + if (_cursorPos < 1) + return; + + // search left until we hit an nonspace character + while (--_cursorPos >= 0) + { + if (!iswspace(m_TextStream[_cursorPos])) + break; + } + + // search left until we hit a whitespace character + while (--_cursorPos >= 0) + { + if (iswspace(m_TextStream[_cursorPos])) + { + break; + } + } + + // we end one character off + _cursorPos++; + + // now we are at the start of the previous word + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Move cursor to the Start of the text buffer +//----------------------------------------------------------------------------- +void RichText::GotoTextStart() +{ + _cursorPos = 0; // set cursor to start + _invalidateVerticalScrollbarSlider = true; + // force scrollbar to the top + _vertScrollBar->SetValue(0); + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Move cursor to the end of the text buffer +//----------------------------------------------------------------------------- +void RichText::GotoTextEnd() +{ + _cursorPos = m_TextStream.Count(); // set cursor to end of buffer + _invalidateVerticalScrollbarSlider = true; + + // force the scrollbar to the bottom + int min, max; + _vertScrollBar->GetRange(min, max); + _vertScrollBar->SetValue(max); + + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Culls the text stream down to a managable size +//----------------------------------------------------------------------------- +void RichText::TruncateTextStream() +{ + if (_maxCharCount < 1) + return; + + // choose a point to cull at + int cullPos = _maxCharCount / 2; + + // kill half the buffer + m_TextStream.RemoveMultiple(0, cullPos); + + // work out where in the format stream we can start + int formatIndex = FindFormatStreamIndexForTextStreamPos(cullPos); + if (formatIndex > 0) + { + // take a copy, make it first + m_FormatStream[0] = m_FormatStream[formatIndex]; + m_FormatStream[0].textStreamIndex = 0; + // kill the others + m_FormatStream.RemoveMultiple(1, formatIndex); + } + + // renormalize the remainder of the format stream + for (int i = 1; i < m_FormatStream.Count(); i++) + { + Assert(m_FormatStream[i].textStreamIndex > cullPos); + m_FormatStream[i].textStreamIndex -= cullPos; + } + + // mark everything to be recalculated + InvalidateLineBreakStream(); + InvalidateLayout(); + _invalidateVerticalScrollbarSlider = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Insert a character into the text buffer +//----------------------------------------------------------------------------- +void RichText::InsertChar(wchar_t wch) +{ + // throw away redundant linefeed characters + if ( wch == '\r' ) + return; + + if (_maxCharCount > 0 && m_TextStream.Count() > _maxCharCount) + { + TruncateTextStream(); + } + + // insert the new char at the end of the buffer + m_TextStream.AddToTail(wch); + + // mark the linebreak steam as needing recalculating from that point + _recalculateBreaksIndex = m_LineBreaks.Count() - 2; + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Insert a string into the text buffer, this is just a series +// of char inserts because we have to check each char is ok to insert +//----------------------------------------------------------------------------- +void RichText::InsertString(const char *text) +{ + if (text[0] == '#') + { + wchar_t unicode[ 1024 ]; + ResolveLocalizedTextAndVariables( text, unicode, sizeof( unicode ) ); + InsertString( unicode ); + return; + } + + // upgrade the ansi text to unicode to display it + int len = strlen(text); + wchar_t *unicode = (wchar_t *)_alloca((len + 1) * sizeof(wchar_t)); + Q_UTF8ToUnicode(text, unicode, ((len + 1) * sizeof(wchar_t))); + InsertString(unicode); +} + +//----------------------------------------------------------------------------- +// Purpose: Insertsa a unicode string into the buffer +//----------------------------------------------------------------------------- +void RichText::InsertString(const wchar_t *wszText) +{ + // insert the whole string + for (const wchar_t *ch = wszText; *ch != 0; ++ch) + { + InsertChar(*ch); + } + InvalidateLayout(); + m_bRecalcLineBreaks = true; + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Declare a selection empty +//----------------------------------------------------------------------------- +void RichText::SelectNone() +{ + // tag the selection as empty + _select[0] = -1; + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Load in the selection range so cx0 is the Start and cx1 is the end +// from smallest to highest (right to left) +//----------------------------------------------------------------------------- +bool RichText::GetSelectedRange(int &cx0, int &cx1) +{ + // if there is nothing selected return false + if (_select[0] == -1) + return false; + + // sort the two position so cx0 is the smallest + cx0 = _select[0]; + cx1 = _select[1]; + if (cx1 < cx0) + { + int temp = cx0; + cx0 = cx1; + cx1 = temp; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Opens the cut/copy/paste dropdown menu +//----------------------------------------------------------------------------- +void RichText::OpenEditMenu() +{ + // get cursor position, this is local to this text edit window + // so we need to adjust it relative to the parent + int cursorX, cursorY; + input()->GetCursorPos(cursorX, cursorY); + + /* !! disabled since it recursively gets panel pointers, potentially across dll boundaries, + and doesn't need to be necessary (it's just for handling windowed mode) + + // find the frame that has no parent (the one on the desktop) + Panel *panel = this; + while ( panel->GetParent() != NULL) + { + panel = panel->GetParent(); + } + panel->ScreenToLocal(cursorX, cursorY); + int x, y; + // get base panel's postition + panel->GetPos(x, y); + + // adjust our cursor position accordingly + cursorX += x; + cursorY += y; + */ + + int x0, x1; + if (GetSelectedRange(x0, x1)) // there is something selected + { + m_pEditMenu->SetItemEnabled("&Cut", true); + m_pEditMenu->SetItemEnabled("C&opy", true); + } + else // there is nothing selected, disable cut/copy options + { + m_pEditMenu->SetItemEnabled("&Cut", false); + m_pEditMenu->SetItemEnabled("C&opy", false); + } + m_pEditMenu->SetVisible(true); + m_pEditMenu->RequestFocus(); + + // relayout the menu immediately so that we know it's size + m_pEditMenu->InvalidateLayout(true); + int menuWide, menuTall; + m_pEditMenu->GetSize(menuWide, menuTall); + + // work out where the cursor is and therefore the best place to put the menu + int wide, tall; + surface()->GetScreenSize(wide, tall); + + if (wide - menuWide > cursorX) + { + // menu hanging right + if (tall - menuTall > cursorY) + { + // menu hanging down + m_pEditMenu->SetPos(cursorX, cursorY); + } + else + { + // menu hanging up + m_pEditMenu->SetPos(cursorX, cursorY - menuTall); + } + } + else + { + // menu hanging left + if (tall - menuTall > cursorY) + { + // menu hanging down + m_pEditMenu->SetPos(cursorX - menuWide, cursorY); + } + else + { + // menu hanging up + m_pEditMenu->SetPos(cursorX - menuWide, cursorY - menuTall); + } + } + + m_pEditMenu->RequestFocus(); +} + +//----------------------------------------------------------------------------- +// Purpose: Cuts the selected chars from the buffer and +// copies them into the clipboard +//----------------------------------------------------------------------------- +void RichText::CutSelected() +{ + CopySelected(); + // have to request focus if we used the menu + RequestFocus(); +} + +//----------------------------------------------------------------------------- +// Purpose: Copies the selected chars into the clipboard +//----------------------------------------------------------------------------- +void RichText::CopySelected() +{ + int x0, x1; + if (GetSelectedRange(x0, x1)) + { + CUtlVector<wchar_t> buf; + for (int i = x0; i <= x1; i++) + { + if ( m_TextStream.IsValidIndex(i) == false ) + continue; + + if (m_TextStream[i] == '\n') + { + buf.AddToTail( '\r' ); + } + // remove any rich edit commands + buf.AddToTail(m_TextStream[i]); + } + buf.AddToTail('\0'); + system()->SetClipboardText(buf.Base(), buf.Count() - 1); + } + + // have to request focus if we used the menu + RequestFocus(); +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the index in the text buffer of the +// character the drawing should Start at +//----------------------------------------------------------------------------- +int RichText::GetStartDrawIndex(int &lineBreakIndexIndex) +{ + int startIndex = 0; + int startLine = _vertScrollBar->GetValue(); + + if ( startLine >= m_LineBreaks.Count() ) // incase the line breaks got reset and the scroll bar hasn't + { + startLine = m_LineBreaks.Count() - 1; + } + + lineBreakIndexIndex = startLine; + if (startLine && startLine < m_LineBreaks.Count()) + { + startIndex = m_LineBreaks[startLine - 1]; + } + + return startIndex; +} + +//----------------------------------------------------------------------------- +// Purpose: Get a string from text buffer +// Input: offset - index to Start reading from +// bufLen - length of string +//----------------------------------------------------------------------------- +void RichText::GetText(int offset, wchar_t *buf, int bufLenInBytes) +{ + if (!buf) + return; + + Assert( bufLenInBytes >= sizeof(buf[0]) ); + int bufLen = bufLenInBytes / sizeof(wchar_t); + int i; + for (i = offset; i < (offset + bufLen - 1); i++) + { + if (i >= m_TextStream.Count()) + break; + + buf[i-offset] = m_TextStream[i]; + } + buf[(i-offset)] = 0; + buf[bufLen-1] = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: gets text from the buffer +//----------------------------------------------------------------------------- +void RichText::GetText(int offset, char *pch, int bufLenInBytes) +{ + wchar_t rgwchT[4096]; + GetText(offset, rgwchT, sizeof(rgwchT)); + Q_UnicodeToUTF8(rgwchT, pch, bufLenInBytes); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the font of the buffer text +//----------------------------------------------------------------------------- +void RichText::SetFont(HFont font) +{ + _font = font; + InvalidateLayout(); + m_bRecalcLineBreaks = true; + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: Called when the scrollbar slider is moved +//----------------------------------------------------------------------------- +void RichText::OnSliderMoved() +{ + _recalcSavedRenderState = true; + Repaint(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool RichText::RequestInfo(KeyValues *outputData) +{ + if (!stricmp(outputData->GetName(), "GetText")) + { + wchar_t wbuf[512]; + GetText(0, wbuf, sizeof(wbuf)); + outputData->SetWString("text", wbuf); + return true; + } + + return BaseClass::RequestInfo(outputData); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::OnSetText(const wchar_t *text) +{ + SetText(text); +} + +//----------------------------------------------------------------------------- +// Purpose: Called when a URL, etc has been clicked on +//----------------------------------------------------------------------------- +void RichText::OnClickPanel(int index) +{ + wchar_t wBuf[512]; + int outIndex = 0; + + // parse out the clickable text, and send it to our listeners + _currentTextClickable = true; + TRenderState renderState; + GenerateRenderStateForTextStreamIndex(index, renderState); + for (int i = index; i < (sizeof(wBuf) - 1) && i < m_TextStream.Count(); i++) + { + // stop getting characters when text is no longer clickable + UpdateRenderState(i, renderState); + if (!renderState.textClickable) + break; + + // copy out the character + wBuf[outIndex++] = m_TextStream[i]; + } + + wBuf[outIndex] = 0; + + int iFormatSteam = FindFormatStreamIndexForTextStreamPos( index ); + if ( m_FormatStream[iFormatSteam].m_sClickableTextAction ) + { + Q_UTF8ToUnicode( m_FormatStream[iFormatSteam].m_sClickableTextAction.String(), wBuf, sizeof( wBuf ) ); + } + + PostActionSignal(new KeyValues("TextClicked", "text", wBuf)); + OnTextClicked(wBuf); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::ApplySettings(KeyValues *inResourceData) +{ + BaseClass::ApplySettings(inResourceData); + SetMaximumCharCount(inResourceData->GetInt("maxchars", -1)); + SetVerticalScrollbar(inResourceData->GetInt("scrollbar", 1)); + + // get the starting text, if any + const char *text = inResourceData->GetString("text", ""); + if (*text) + { + delete [] m_pszInitialText; + int len = Q_strlen(text) + 1; + m_pszInitialText = new char[ len ]; + Q_strncpy( m_pszInitialText, text, len ); + SetText(text); + } + else + { + const char *textfilename = inResourceData->GetString("textfile", NULL); + if ( textfilename ) + { + FileHandle_t f = g_pFullFileSystem->Open( textfilename, "rt" ); + if (!f) + { + Warning( "RichText: textfile parameter '%s' not found.\n", textfilename ); + return; + } + + int len = g_pFullFileSystem->Size( f ); + delete [] m_pszInitialText; + m_pszInitialText = new char[ len + 1 ]; + g_pFullFileSystem->Read( m_pszInitialText, len, f ); + m_pszInitialText[len - 1] = 0; + SetText( m_pszInitialText ); + + g_pFullFileSystem->Close( f ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::GetSettings(KeyValues *outResourceData) +{ + BaseClass::GetSettings(outResourceData); + outResourceData->SetInt("maxchars", _maxCharCount); + outResourceData->SetInt("scrollbar", _vertScrollBar->IsVisible() ); + if (m_pszInitialText) + { + outResourceData->SetString("text", m_pszInitialText); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +const char *RichText::GetDescription() +{ + static char buf[1024]; + Q_snprintf(buf, sizeof(buf), "%s, string text, bool scrollbar", BaseClass::GetDescription()); + return buf; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the number of lines in the window +//----------------------------------------------------------------------------- +int RichText::GetNumLines() +{ + return m_LineBreaks.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the height of the text entry window so all text will fit inside +//----------------------------------------------------------------------------- +void RichText::SetToFullHeight() +{ + PerformLayout(); + int wide, tall; + GetSize(wide, tall); + + tall = GetNumLines() * (GetLineHeight() + _drawOffsetY) + _drawOffsetY + 2; + SetSize (wide, tall); + PerformLayout(); +} + +//----------------------------------------------------------------------------- +// Purpose: Select all the text. +//----------------------------------------------------------------------------- +void RichText::SelectAllText() +{ + _cursorPos = 0; + _select[0] = 0; + _select[1] = m_TextStream.Count(); +} + +//----------------------------------------------------------------------------- +// Purpose: Select all the text. +//----------------------------------------------------------------------------- +void RichText::SelectNoText() +{ + _select[0] = 0; + _select[1] = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::OnSetFocus() +{ + BaseClass::OnSetFocus(); +} + +//----------------------------------------------------------------------------- +// Purpose: Invalidates the current linebreak stream +//----------------------------------------------------------------------------- +void RichText::InvalidateLineBreakStream() +{ + // clear the buffer + m_LineBreaks.RemoveAll(); + m_LineBreaks.AddToTail(MAX_BUFFER_SIZE); + _recalculateBreaksIndex = 0; + m_bRecalcLineBreaks = true; +} + +//----------------------------------------------------------------------------- +// Purpose: Inserts a text string while making URLs clickable/different color +// Input : *text - string that may contain URLs to make clickable/color coded +// URLTextColor - color for URL text +// normalTextColor - color for normal text +//----------------------------------------------------------------------------- +void RichText::InsertPossibleURLString(const char* text, Color URLTextColor, Color normalTextColor) +{ + InsertColorChange(normalTextColor); + + // parse out the string for URL's + int len = Q_strlen(text), pos = 0; + bool clickable = false; + char *pchURLText = (char *)stackalloc( len + 1 ); + char *pchURL = (char *)stackalloc( len + 1 ); + + while (pos < len) + { + pos = ParseTextStringForUrls( text, pos, pchURLText, len, pchURL, len, clickable ); + + if ( clickable ) + { + InsertClickableTextStart( pchURL ); + InsertColorChange( URLTextColor ); + } + + InsertString( pchURLText ); + + if ( clickable ) + { + InsertColorChange(normalTextColor); + InsertClickableTextEnd(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: looks for URLs in the string and returns information about the URL +//----------------------------------------------------------------------------- +int RichText::ParseTextStringForUrls( const char *text, int startPos, char *pchURLText, int cchURLText, char *pchURL, int cchURL, bool &clickable ) +{ + // scan for text that looks like a URL + int i = startPos; + while (text[i] != 0) + { + bool bURLFound = false; + + if ( !Q_strnicmp(text + i, "<a href=", 8) ) + { + if (i > startPos) + break; + + // embedded link + bURLFound = true; + clickable = true; + // get the url + i += Q_strlen( "<a href=" ); + const char *pchURLEnd = Q_strstr( text + i, ">" ); + Q_strncpy( pchURL, text + i, min( pchURLEnd - text - i + 1, cchURL ) ); + i += ( pchURLEnd - text - i + 1 ); + + // get the url text + pchURLEnd = Q_strstr( text, "</a>" ); + Q_strncpy( pchURLText, text + i, min( pchURLEnd - text - i + 1, cchURLText ) ); + i += ( pchURLEnd - text - i ); + i += Q_strlen( "</a>" ); + + // we're done + return i; + } + else if (!Q_strnicmp(text + i, "www.", 4)) + { + // scan ahead for another '.' + bool bPeriodFound = false; + for (const char *ch = text + i + 5; ch != 0; ch++) + { + if (*ch == '.') + { + bPeriodFound = true; + break; + } + } + + // URL found + if (bPeriodFound) + { + bURLFound = true; + } + } + else if (!Q_strnicmp(text + i, "http://", 7)) + { + bURLFound = true; + } + else if (!Q_strnicmp(text + i, "ftp://", 6)) + { + bURLFound = true; + } + else if (!Q_strnicmp(text + i, "steam://", 8)) + { + bURLFound = true; + } + else if (!Q_strnicmp(text + i, "steambeta://", 12)) + { + bURLFound = true; + } + else if (!Q_strnicmp(text + i, "mailto:", 7)) + { + bURLFound = true; + } + else if (!Q_strnicmp(text + i, "\\\\", 2)) + { + bURLFound = true; + } + + if (bURLFound) + { + if (i == startPos) + { + // we're at the Start of a URL, so parse that out + clickable = true; + int outIndex = 0; + while (text[i] != 0 && !iswspace(text[i])) + { + pchURLText[outIndex++] = text[i++]; + } + pchURLText[outIndex] = 0; + Q_strncpy( pchURL, pchURLText, cchURL ); + return i; + } + else + { + // no url + break; + } + } + + // increment and loop + i++; + } + + // nothing found; + // parse out the text before the end + clickable = false; + int outIndex = 0; + int fromIndex = startPos; + while ( fromIndex < i && outIndex < cchURLText ) + { + pchURLText[outIndex++] = text[fromIndex++]; + } + pchURLText[outIndex] = 0; + Q_strncpy( pchURL, pchURLText, cchURL ); + + return i; +} + +//----------------------------------------------------------------------------- +// Purpose: Executes the text-clicked command, which opens a web browser by +// default. +//----------------------------------------------------------------------------- +void RichText::OnTextClicked(const wchar_t *wszText) +{ + // Strip leading/trailing quotes, which may be present on href tags or may not. + const wchar_t *pwchURL = wszText; + if ( pwchURL[0] == L'"' || pwchURL[0] == L'\'' ) + pwchURL = wszText + 1; + + char ansi[2048]; + Q_UnicodeToUTF8( pwchURL, ansi, sizeof(ansi) ); + + size_t strLen = Q_strlen(ansi); + if ( strLen && ( ansi[strLen-1] == '"' || ansi[strLen] == '\'' ) ) + { + ansi[strLen-1] = 0; + } + + if ( m_hPanelToHandleClickingURLs.Get() ) + { + PostMessage( m_hPanelToHandleClickingURLs.Get(), new KeyValues( "URLClicked", "url", ansi ) ); + } + else + { + system()->ShellExecute( "open", ansi ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void RichText::SetURLClickedHandler( Panel *pPanelToHandleClickMsg ) +{ + m_hPanelToHandleClickingURLs = pPanelToHandleClickMsg; +} + + +//----------------------------------------------------------------------------- +// Purpose: data accessor +//----------------------------------------------------------------------------- +bool RichText::IsScrollbarVisible() +{ + return _vertScrollBar->IsVisible(); +} + +void RichText::SetUnderlineFont( HFont font ) +{ + m_hFontUnderline = font; +} + +bool RichText::IsAllTextAlphaZero() const +{ + return m_bAllTextAlphaIsZero; +} + +bool RichText::HasText() const +{ + int c = m_TextStream.Count(); + if ( c == 0 ) + { + return false; + } + return true; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Returns the height of the base font +//----------------------------------------------------------------------------- +int RichText::GetLineHeight() +{ + return surface()->GetFontTall( _font ); +} + + +#ifdef DBGFLAG_VALIDATE +//----------------------------------------------------------------------------- +// Purpose: Run a global validation pass on all of our data structures and memory +// allocations. +// Input: validator - Our global validator object +// pchName - Our name (typically a member var in our container) +//----------------------------------------------------------------------------- +void RichText::Validate( CValidator &validator, char *pchName ) +{ + validator.Push( "vgui::RichText", this, pchName ); + + ValidateObj( m_TextStream ); + ValidateObj( m_FormatStream ); + ValidateObj( m_LineBreaks ); + ValidateObj( _clickableTextPanels ); + validator.ClaimMemory( m_pszInitialText ); + + BaseClass::Validate( validator, "vgui::RichText" ); + + validator.Pop(); +} +#endif // DBGFLAG_VALIDATE + |