From f56bb35301836e56582a575a75864392a0177875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20P=2E=20Tjern=C3=B8?= Date: Mon, 2 Dec 2013 19:31:46 -0800 Subject: Fix line endings. WHAMMY. --- mp/src/vgui2/vgui_controls/RichText.cpp | 5488 +++++++++++++++---------------- 1 file changed, 2744 insertions(+), 2744 deletions(-) (limited to 'mp/src/vgui2/vgui_controls/RichText.cpp') 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( static_cast( DRAW_OFFSET_X )*( static_cast( sw )/ static_cast( width ))); - _drawOffsetY = static_cast( static_cast( DRAW_OFFSET_Y )*( static_cast( sw )/ static_cast( 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 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, "" ); - Q_strncpy( pchURL, text + i, min( pchURLEnd - text - i + 1, cchURL ) ); - i += ( pchURLEnd - text - i + 1 ); - - // get the url text - pchURLEnd = Q_strstr( text, "" ); - Q_strncpy( pchURLText, text + i, min( pchURLEnd - text - i + 1, cchURLText ) ); - i += ( pchURLEnd - text - i ); - i += Q_strlen( "" ); - - // 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( static_cast( DRAW_OFFSET_X )*( static_cast( sw )/ static_cast( width ))); + _drawOffsetY = static_cast( static_cast( DRAW_OFFSET_Y )*( static_cast( sw )/ static_cast( 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 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, "" ); + Q_strncpy( pchURL, text + i, min( pchURLEnd - text - i + 1, cchURL ) ); + i += ( pchURLEnd - text - i + 1 ); + + // get the url text + pchURLEnd = Q_strstr( text, "" ); + Q_strncpy( pchURLText, text + i, min( pchURLEnd - text - i + 1, cchURLText ) ); + i += ( pchURLEnd - text - i ); + i += Q_strlen( "" ); + + // 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 + -- cgit v1.2.3