You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2747 lines
76 KiB
2747 lines
76 KiB
//========= 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; |
|
|
|
if ( m_FormatStream.IsValidIndex( renderState.formatStreamIndex ) ) |
|
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 ignX, int ignY) |
|
{ |
|
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 |
|
|
|
|