text: //========= Copyright Valve Corporation, All rights reserved. ============//
  //
  // Purpose:
  //
  //=============================================================================//
  
  
  #include "cbase.h"
  
  #if defined( REPLAY_ENABLED )
  
  #include "replayperformanceeditor.h"
  #include "replay/replay.h"
  #include "replay/ireplayperformanceeditor.h"
  #include "replay/ireplayperformancecontroller.h"
  #include "replay/performance.h"
  #include "ienginevgui.h"
  #include "iclientmode.h"
  #include "vgui_controls/ImagePanel.h"
  #include "vgui_controls/TextImage.h"
  #include "vgui_controls/Slider.h"
  #include "vgui_controls/Menu.h"
  #include "vgui/ILocalize.h"
  #include "vgui/IImage.h"
  #include "c_team.h"
  #include "vgui_avatarimage.h"
  #include "vgui/ISurface.h"
  #include "vgui/IInput.h"
  #include "replay/replaycamera.h"
  #include "replay/ireplaymanager.h"
  #include "replay/iclientreplaycontext.h"
  #include "confirm_dialog.h"
  #include "replayperformancesavedlg.h"
  #include "replay/irecordingsessionmanager.h"
  #include "achievementmgr.h"
  #include "c_playerresource.h"
  #include "replay/gamedefs.h"
  
  // memdbgon must be the last include file in a .cpp file!!!
  #include <tier0/memdbgon.h>
  
  extern CAchievementMgr g_AchievementMgrTF;
  
  //-----------------------------------------------------------------------------
  
  using namespace vgui;
  
  //-----------------------------------------------------------------------------
  
  extern IReplayPerformanceController *g_pReplayPerformanceController;
  
  //-----------------------------------------------------------------------------
  
  // Hack-y global bool to communicate when we are rewinding for map load screens.
  // Order of operations issues preclude the use of engine->IsPlayingDemo().
  bool g_bIsReplayRewinding = false;
  
  //-----------------------------------------------------------------------------
  
  // TODO: Make these archive?  Right now, the tips are reset every time the game starts
  ConVar replay_perftip_count_enter( "replay_perftip_count_enter", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_HIDDEN, "", true, 0, false, 0 );
  ConVar replay_perftip_count_exit( "replay_perftip_count_exit", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_HIDDEN, "", true, 0, false, 0 );
  ConVar replay_perftip_count_freecam_enter( "replay_perftip_count_freecam_enter", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_HIDDEN, "", true, 0, false, 0 );
  ConVar replay_perftip_count_freecam_exit( "replay_perftip_count_freecam_exit", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_HIDDEN, "", true, 0, false, 0 );
  ConVar replay_perftip_count_freecam_exit2( "replay_perftip_count_freecam_exit2", "0", FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_HIDDEN, "", true, 0, false, 0 );
  
  ConVar replay_editor_fov_mousewheel_multiplier( "replay_editor_fov_mousewheel_multiplier", "5", FCVAR_ARCHIVE | FCVAR_CLIENTDLL | FCVAR_DONTRECORD, "The multiplier on mousewheel input for adjusting camera FOV in the replay editor." );
  ConVar replay_editor_fov_mousewheel_invert( "replay_editor_fov_mousewheel_invert", "0", FCVAR_ARCHIVE | FCVAR_CLIENTDLL | FCVAR_DONTRECORD, "Invert FOV zoom/unzoom on mousewheel in the replay editor." );
  
  ConVar replay_replayeditor_rewindmsgcounter( "replay_replayeditor_rewindmsgcounter", "0", FCVAR_ARCHIVE | FCVAR_CLIENTDLL | FCVAR_DONTRECORD | FCVAR_HIDDEN, "" );
  
  //-----------------------------------------------------------------------------
  
  #define MAX_TIP_DISPLAYS		1
  
  //-----------------------------------------------------------------------------
  
  #define TIMESCALE_MIN			0.01f
  #define TIMESCALE_MAX			3.0f
  
  //-----------------------------------------------------------------------------
  
  #define SLIDER_RANGE_MAX		10000.0f
  
  //-----------------------------------------------------------------------------
  
  #define REPLAY_SOUND_DIALOG_POPUP		"replay\\replaydialog_warn.wav"
  
  //-----------------------------------------------------------------------------
  
  static const char *gs_pCamNames[ NCAMS ] =
  {
  	"free",
  	"third",
  	"first",
  	"timescale",
  };
  
  static const char *gs_pBaseComponentNames[ NCAMS ] =
  {
  	"replay/replay_camera_%s%s",
  	"replay/replay_camera_%s%s",
  	"replay/replay_camera_%s%s",
  	"replay/replay_%s%s",
  };
  
  //-----------------------------------------------------------------------------
  
  void PlayDemo()
  {
  	engine->ClientCmd_Unrestricted( "demo_resume" );
  }
  
  void PauseDemo()
  {
  	engine->ClientCmd_Unrestricted( "demo_pause" );
  }
  
  //-----------------------------------------------------------------------------
  
  inline float SCurve( float t )
  {
  	t = clamp( t, 0.0f, 1.0f );
  	return t * t * (3 - 2*t);
  }
  
  inline float CubicEaseIn( float t )
  {
  	t = clamp( t, 0.0f, 1.0f );
  	return t * t * t;
  }
  
  inline float LerpScale( float flIn, float flInMin, float flInMax, float flOutMin, float flOutMax )
  {
  	float flDenom = flInMax - flInMin;
  	if ( flDenom == 0.0f )
  		return 0.0f;
  
  	float t = clamp( ( flIn - flInMin ) / flDenom, 0.0f, 1.0f );
  	return Lerp( t, flOutMin, flOutMax );
  }
  
  //-----------------------------------------------------------------------------
  
  void HighlightTipWords( Label *pLabel )
  {
  	// Setup coloring - get # of words that should be highlighted
  	wchar_t *pwNumWords = g_pVGuiLocalize->Find( "#Replay_PerfTip_Highlight_NumWords" );
  	if ( !pwNumWords )
  		return;
  
  	// Get the current label text
  	wchar_t wszLabelText[512];
  	pLabel->GetText( wszLabelText, sizeof( wszLabelText ) );
  
  	pLabel->GetTextImage()->ClearColorChangeStream();
  	pLabel->GetTextImage()->AddColorChange( pLabel->GetFgColor(), 0 );
  
  	int nNumWords = _wtoi( pwNumWords );
  	for ( int i = 0; i < nNumWords; ++i )
  	{
  		char szWordFindStr[64];
  		V_snprintf( szWordFindStr, sizeof( szWordFindStr ), "#Replay_PerfTip_Highlight_Word%i", i );
  		wchar_t *pwWord = g_pVGuiLocalize->Find( szWordFindStr );
  		if ( !pwWord )
  			continue;
  
  		const int nWordLen = wcslen( pwWord );
  
  		// Find any instance of the word in the label text and highlight it in red
  		const wchar_t *p = wszLabelText;
  		do 
  		{
  			const wchar_t *pInst = wcsstr( p, pwWord );
  			if ( !pInst )
  				break;
  
  			// Highlight the text
  			int nStartPos = pInst - wszLabelText;
  			int nEndPos = nStartPos + nWordLen;
  
  			// If start pos is non-zero, clear color changes
  			bool bChangeColor = true;
  			if ( nStartPos == 0 )
  			{
  				pLabel->GetTextImage()->ClearColorChangeStream();
  			}
  			else if ( iswalpha( wszLabelText[ nStartPos - 1 ] ) )
  			{
  				// If this is not the beginning of the string, check the previous character.  If it's
  				// not whitespace, etc, we found an instance of a keyword within another word.  Skip.
  				bChangeColor = false;
  			}
  
  			if ( bChangeColor )
  			{
  				pLabel->GetTextImage()->AddColorChange( Color(200,80,60,255), nStartPos );
  				pLabel->GetTextImage()->AddColorChange( pLabel->GetFgColor(), nEndPos );
  			}
  
  			p = pInst + nWordLen;
  		} while ( 1 );
  	}
  }
  
  //-----------------------------------------------------------------------------
  
  class CSavingDialog : public CGenericWaitingDialog
  {
  	DECLARE_CLASS_SIMPLE( CSavingDialog, CGenericWaitingDialog );
  public:
  	CSavingDialog( CReplayPerformanceEditorPanel *pEditorPanel ) 
  	:	CGenericWaitingDialog( pEditorPanel )
  	{
  		m_pEditorPanel = pEditorPanel;
  	}
  
  	virtual void OnTick()
  	{
  		BaseClass::OnTick();
  
  		if ( !g_pReplayPerformanceController )
  			return;
  
  		// Update async save
  		if ( g_pReplayPerformanceController->IsSaving() )
  		{
  			g_pReplayPerformanceController->SaveThink();
  		}
  		else
  		{
  			if ( m_pEditorPanel.Get() )
  			{
  				m_pEditorPanel->OnSaveComplete();
  			}
  
  			Close();
  		}
  	}
  
  private:
  	CConfirmDialog *m_pLoginDialog;
  	vgui::DHANDLE< CReplayPerformanceEditorPanel > m_pEditorPanel;
  };
  
  //-----------------------------------------------------------------------------
  
  class CReplayTipLabel : public Label
  {
  	DECLARE_CLASS_SIMPLE( CReplayTipLabel, Label );
  public:
  	CReplayTipLabel( Panel *pParent, const char *pName, const char *pText )
  	:	BaseClass( pParent, pName, pText )
  	{
  	}
  
  	virtual void ApplySchemeSettings( IScheme *pScheme )
  	{
  		BaseClass::ApplySchemeSettings( pScheme );
  		HighlightTipWords( this );
  	}
  };
  
  DECLARE_BUILD_FACTORY_DEFAULT_TEXT( CReplayTipLabel, Label );
  
  //-----------------------------------------------------------------------------
  
  class CPerformanceTip : public EditablePanel
  {
  	DECLARE_CLASS_SIMPLE( CPerformanceTip, EditablePanel );
  public:
  	static DHANDLE< CPerformanceTip > s_pTip;
  
  	static CPerformanceTip *CreateInstance( const char *pText )
  	{
  		if ( s_pTip )
  		{
  			s_pTip->SetVisible( false );
  			s_pTip->MarkForDeletion();
  			s_pTip = NULL;
  		}
  
  		s_pTip = SETUP_PANEL( new CPerformanceTip( pText ) );
  
  		return s_pTip;
  	}
  
  	CPerformanceTip( const char *pText )
  	:	BaseClass( g_pClientMode->GetViewport(), "Tip" ),
  		m_flBornTime( gpGlobals->realtime ),
  		m_flAge( 0.0f ),
  		m_flShowDuration( 15.0f )
  	{
  		m_pTextLabel = new CReplayTipLabel( this, "TextLabel", pText );
  	}
  	
  	virtual void OnThink()
  	{
  		// Delete the panel if life exceeded
  		const float flEndTime = m_flBornTime + m_flShowDuration;
  		if ( gpGlobals->realtime >= flEndTime )
  		{
  			SetVisible( false );
  			MarkForDeletion();
  			s_pTip = NULL;
  			return;
  		}
  
  		SetVisible( true );
  
  		const float flFadeDuration = .4f;
  		float flAlpha;
  
  		// Fade out?
  		if ( gpGlobals->realtime >= flEndTime - flFadeDuration )
  		{
  			flAlpha = LerpScale( gpGlobals->realtime, flEndTime - flFadeDuration, flEndTime, 1.0f, 0.0f );
  		}
  
  		// Fade in?
  		else if ( gpGlobals->realtime <= m_flBornTime + flFadeDuration )
  		{
  			flAlpha = LerpScale( gpGlobals->realtime, m_flBornTime, m_flBornTime + flFadeDuration, 0.0f, 1.0f );
  		}
  
  		// Otherwise, we must be in between fade in/fade out
  		else
  		{
  			flAlpha = 1.0f;
  		}
  
  		SetAlpha( 255 * SCurve( flAlpha ) );
  	}
  
  	virtual void ApplySchemeSettings( IScheme *pScheme )
  	{
  		BaseClass::ApplySchemeSettings( pScheme );
  
  		LoadControlSettings( "resource/ui/replayperformanceeditor/tip.res", "GAME" );
  
  		// Center relative to parent
  		const int nScreenW = ScreenWidth();
  		const int nScreenH = ScreenHeight();
  		int aContentSize[2];
  		m_pTextLabel->GetContentSize( aContentSize[0], aContentSize[1] );
  		const int nLabelHeight = aContentSize[1];
  		SetBounds( 
  			0,
  			3 * nScreenH / 4 - nLabelHeight / 2,
  			nScreenW,
  			nLabelHeight + 2 * m_nTopBottomMargin
  		);
  		m_pTextLabel->SetBounds(
  			m_nLeftRightMarginWidth,
  			m_nTopBottomMargin,
  			nScreenW - 2 * m_nLeftRightMarginWidth,
  			nLabelHeight
  		);
  	}
  
  	static void Cleanup()
  	{
  		if ( s_pTip )
  		{
  			s_pTip->MarkForDeletion();
  			s_pTip = NULL;
  		}
  	}
  
  	CPanelAnimationVarAliasType( int, m_nLeftRightMarginWidth, "left_right_margin", "0", "proportional_xpos" );
  	CPanelAnimationVarAliasType( int, m_nTopBottomMargin , "top_bottom_margin", "0", "proportional_ypos" );
  
  	CReplayTipLabel	*m_pTextLabel;
  	float			m_flBornTime;
  	float			m_flAge;
  	float			m_flShowDuration;
  };
  
  DHANDLE< CPerformanceTip > CPerformanceTip::s_pTip;
  
  // Display the performance tip if we haven't already displayed it nMaxTimesToDisplay times or more
  inline void DisplayPerformanceTip( const char *pText, ConVar* pCountCv = NULL, int nMaxTimesToDisplay = -1 )
  {
  	// Already displayed too many times?  Get out.
  	if ( pCountCv && nMaxTimesToDisplay >= 0 )
  	{
  		int nCount = pCountCv->GetInt();
  		if ( nCount >= nMaxTimesToDisplay )
  			return;
  
  		// Incremement count cvar
  		pCountCv->SetValue( nCount + 1 );
  	}
  
  	// Display the tip
  	CPerformanceTip::CreateInstance( pText );
  }
  
  //-----------------------------------------------------------------------------
  
  inline float GetPlaybackTime()
  {
  	CReplay *pPlayingReplay = g_pReplayManager->GetPlayingReplay();
  	return gpGlobals->curtime - TICKS_TO_TIME( pPlayingReplay->m_nSpawnTick );
  }
  
  //-----------------------------------------------------------------------------
  
  class CPlayerCell : public CExImageButton
  {
  	DECLARE_CLASS_SIMPLE( CPlayerCell, CExImageButton );
  public:
  	CPlayerCell( Panel *pParent, const char *pName, int *pCurTargetPlayerIndex )
  	:	CExImageButton( pParent, pName, "" ),
  		m_iPlayerIndex( -1 ),
  		m_pCurTargetPlayerIndex( pCurTargetPlayerIndex )
  	{
  	}
  
  	virtual void ApplySchemeSettings( IScheme *pScheme )
  	{
  		BaseClass::ApplySchemeSettings( pScheme );
  
  		GetImage()->SetImage( "" );
  		SetFont( pScheme->GetFont( "ReplaySmall" ) );
  		SetContentAlignment( Label::a_center );
  	}
  
  	MESSAGE_FUNC( DoClick, "PressButton" )
  	{
  		ReplayCamera()->SetPrimaryTarget( m_iPlayerIndex );
  		*m_pCurTargetPlayerIndex = m_iPlayerIndex;
  
  		float flCurTime = GetPlaybackTime();
  
  		extern IReplayPerformanceController *g_pReplayPerformanceController;
  		g_pReplayPerformanceController->AddEvent_Camera_ChangePlayer( flCurTime, m_iPlayerIndex );
  	}
  
  	int		m_iPlayerIndex;
  	int		*m_pCurTargetPlayerIndex;	// Allow the button to write current target in outer class when pressed
  };
  
  //-----------------------------------------------------------------------------
  
  /*
  class CReplayEditorSlider : public Slider
  {
  	DECLARE_CLASS_SIMPLE( CReplayEditorSlider, Slider );
  public:
  	CReplayEditorSlider( Panel *pParent, const char *pName )
  	:	Slider( pParent, pName )
  	{
  	}
  
  	virtual void SetDefault( float flDefault ) { m_flDefault = flDefault; }
  
  	ON_MESSAGE( Reset, OnReset )
  	{
  		SetValue( 
  	}
  
  private:
  	float	m_flDefault;
  };
  */
  
  //-----------------------------------------------------------------------------
  
  class CCameraOptionsPanel : public EditablePanel
  {
  	DECLARE_CLASS_SIMPLE( CCameraOptionsPanel, EditablePanel );
  public:
  	CCameraOptionsPanel( Panel *pParent, const char *pName, const char *pTitle )
  	:	EditablePanel( pParent, pName ),
  		m_bControlsAdded( false )
  	{
  		m_pTitleLabel = new CExLabel( this, "TitleLabel", pTitle );
  
  		AddControlToLayout( m_pTitleLabel );
  	}
  
  	~CCameraOptionsPanel()
  	{
  		m_lstSliderInfos.PurgeAndDeleteElements();
  	}
  
  	void AddControlToLayout( Panel *pControl )
  	{
  		if ( pControl )
  		{
  			m_lstControls.AddToTail( pControl );
  			pControl->SetMouseInputEnabled( true );
  		}
  	}
  
  	// NOTE: Default value is assumed to be stored in flOut
  	void AddSliderToLayout( int nId, Slider *pSlider, const char *pLabelText,
  							float flMinValue, float flMaxValue, float &flOut )
  	{
  		SliderInfo_t *pNewSliderInfo = new SliderInfo_t;
  
  		pNewSliderInfo->m_nId = nId;
  		pNewSliderInfo->m_pSlider = pSlider;
  		pNewSliderInfo->m_flRange[ 0 ] = flMinValue;
  		pNewSliderInfo->m_flRange[ 1 ] = flMaxValue;
  		pNewSliderInfo->m_flDefault = flOut;
  		pNewSliderInfo->m_pValueOut = &flOut;
  
  		m_lstSliderInfos.AddToTail( pNewSliderInfo );
  
  		AddControlToLayout( new EditablePanel( this, "Buffer" ) );
  		AddControlToLayout( NewLabel( pLabelText ) );
  		AddControlToLayout( NewSetDefaultButton( nId ) );
  		AddControlToLayout( pSlider );
  
  		pSlider->AddActionSignalTarget( this );
  	}
  
  	void ResetSlider( int nId )
  	{
  		const SliderInfo_t *pSliderInfo = FindSliderInfoFromId( nId );
  		if ( !pSliderInfo )
  			return;
  
  		SetValue( pSliderInfo, pSliderInfo->m_flDefault );
  	}
  
  	void SetValue( int nId, float flValue )
  	{
  		const SliderInfo_t *pSliderInfo = FindSliderInfoFromId( nId );
  		if ( !pSliderInfo )
  			return;
  
  		SetValue( pSliderInfo, flValue );
  	}
  
  	virtual void ApplySchemeSettings( IScheme *pScheme )
  	{
  		BaseClass::ApplySchemeSettings( pScheme );
  
  		// Setup border
  		SetBorder( pScheme->GetBorder( "ButtonBorder" ) );
  
  		HFont hFont = pScheme->GetFont( "ReplayBrowserSmallest", true );
  		m_pTitleLabel->SetFont( hFont );
  		m_pTitleLabel->SizeToContents();
  		m_pTitleLabel->SetTall( YRES( 20 ) );
  		m_pTitleLabel->SetColorStr( "235 235 235 255" );
  
  		if ( !m_bControlsAdded )
  		{
              const char *pResFile = GetResFile();
              if ( pResFile )
              {
  				LoadControlSettings( pResFile, "GAME" );
  			}
  
  			AddControls();
  			m_bControlsAdded = true;
  		}
  
  		FOR_EACH_LL( m_lstSliderInfos, it )
  		{
  			SliderInfo_t *pInfo = m_lstSliderInfos[ it ];
  			Slider *pSlider = pInfo->m_pSlider;
  			pSlider->SetRange( 0, SLIDER_RANGE_MAX );
  			pSlider->SetNumTicks( 10 );
  			float flDenom = fabs( pInfo->m_flRange[1] - pInfo->m_flRange[0] );
  			pSlider->SetValue( SLIDER_RANGE_MAX * fabs( pInfo->m_flDefault - pInfo->m_flRange[0] ) / flDenom );
  		}
  	}
  
  	virtual void PerformLayout()
  	{
  		BaseClass::PerformLayout();
  
  		int nWidth = XRES( 140 );
  		int nMargins[2] = { (int)XRES( 5 ), (int)YRES( 5 ) };
  		int nVBuf = YRES( 0 );
  		int nLastY = -1;
  		int nY = nMargins[1];
  		Panel *pPrevPanel = NULL;
  		int nLastCtrlHeight = 0;
  
  		FOR_EACH_LL( m_lstControls, i )
  		{
  			Panel *pPanel = m_lstControls[ i ];
  			if ( !pPanel->IsVisible() )
  				continue;
  
  			int aPos[2];
  			pPanel->GetPos( aPos[0], aPos[1] );
  
  			if ( pPrevPanel && aPos[1] >= 0 )
  			{
  				nY += pPrevPanel->GetTall() + nVBuf;
  			}
  
  			// Gross hack to see if the control is a default button
  			if ( dynamic_cast< CExButton * >( pPanel ) )
  			{
  				pPanel->SetWide( XRES( 36 ) );
  				pPanel->SetPos( pPrevPanel ? ( GetWide() - nMargins[0] - pPanel->GetWide() ) : 0, nLastY );
  			}
  			else
  			{
  				pPanel->SetWide( nWidth - 2 * nMargins[0] );
  				pPanel->SetPos( nMargins[0], nY );
  			}
  
  			nLastY = nY;
  			pPrevPanel = pPanel;
  			nLastCtrlHeight = MAX( nLastCtrlHeight, pPanel->GetTall() );
  		}
  
  		SetSize( nWidth, nY + nLastCtrlHeight + 2 * YRES( 3 ) );
  	}
  
  	virtual void OnCommand( const char *pCommand )
  	{
  		if ( !V_strnicmp( pCommand, "reset_", 6 ) )
  		{
  			const int nSliderInfoId = atoi( pCommand + 6 );
  			ResetSlider( nSliderInfoId );
  		}
  		else
  		{
  			BaseClass::OnCommand( pCommand );
  		}
  	}
  
  	Label *NewLabel( const char *pText )
  	{
  		Label *pLabel = new Label( this, "Label", pText );
  		pLabel->SetTall( YRES( 9 ) );
  		pLabel->SetPos( -1, 0 );	// Use default x and accumulated y
  
  		// Set font
  		IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() );
  		HFont hFont = pScheme->GetFont( "DefaultVerySmall", true );
  		pLabel->SetFont( hFont );
  
  		return pLabel;
  	}
  
  	CExButton *NewSetDefaultButton( int nSliderInfoId )
  	{
  		CExButton *pButton = new CExButton( this, "DefaultButton", "#Replay_SetDefaultSetting" );
  		pButton->SetTall( YRES( 11 ) );
  		pButton->SetPos( XRES( 30 ), -1 );	// Set y to -1 so it will stay on the same line
  		pButton->SetContentAlignment( Label::a_center );
  		CFmtStr fmtResetCommand( "reset_%i", nSliderInfoId );
  		pButton->SetCommand( fmtResetCommand.Access() );
  		pButton->AddActionSignalTarget( this );
  
  		// Set font
  		IScheme *pScheme = vgui::scheme()->GetIScheme( GetScheme() );
  		HFont hFont = pScheme->GetFont( "DefaultVerySmall", true );
  		pButton->SetFont( hFont );
  
  		return pButton;
  	}
  
  protected:
  	MESSAGE_FUNC_PARAMS( OnSliderMoved, "SliderMoved", pParams )
  	{
  		Panel *pSlider = (Panel *)pParams->GetPtr( "panel" );
  		float flPercent = pParams->GetInt( "position" ) / SLIDER_RANGE_MAX;
  
  		FOR_EACH_LL( m_lstSliderInfos, it )
  		{
  			SliderInfo_t *pInfo = m_lstSliderInfos[ it ];
  			if ( pSlider == pInfo->m_pSlider )
  			{
  				*pInfo->m_pValueOut = Lerp( flPercent, pInfo->m_flRange[0], pInfo->m_flRange[1] );
  			}
  		}
  	}
  
  	virtual const char *GetResFile() { return NULL; }
  
  	virtual void AddControls()
  	{
  	}
  
  	struct SliderInfo_t
  	{
  		Slider	*m_pSlider;
  		float	m_flRange[2];
  		float	m_flDefault;
  		int		m_nId;
  		float	*m_pValueOut;
  	};
  
  	const SliderInfo_t *FindSliderInfoFromId( int nId )
  	{
  		FOR_EACH_LL( m_lstSliderInfos, it )
  		{
  			SliderInfo_t *pInfo = m_lstSliderInfos[ it ];
  			if ( pInfo->m_nId == nId )
  				return pInfo;
  		}
  
  		AssertMsg( 0, "Should always find a slider here." );
  
  		return NULL;
  	}
  
  	void SetValue( const SliderInfo_t *pSliderInfo, float flValue )
  	{
  		if ( !pSliderInfo )
  		{
  			AssertMsg( 0, "This should not happen." );
  			return;
  		}
  
  		// Calculate the range
  		const float flRange = fabs( pSliderInfo->m_flRange[1] - pSliderInfo->m_flRange[0] );
  		AssertMsg( flRange > 0, "Bad slider range!" );
  
  		// Calculate the percentile based on the specified value and the range.
  		const float flPercent = fabs( flValue - pSliderInfo->m_flRange[0] ) / flRange;
  		pSliderInfo->m_pSlider->SetValue( flPercent * SLIDER_RANGE_MAX, true );
  	}
  
  	CUtlLinkedList< Panel * >				m_lstControls;
  	CUtlLinkedList< SliderInfo_t *, int >	m_lstSliderInfos;
  	CExLabel								*m_pTitleLabel;
  	bool									m_bControlsAdded;
  };
  
  //-----------------------------------------------------------------------------
  
  class CTimeScaleOptionsPanel : public CCameraOptionsPanel
  {
  	DECLARE_CLASS_SIMPLE( CTimeScaleOptionsPanel, CCameraOptionsPanel );
  public:
  	CTimeScaleOptionsPanel( Panel *pParent, float *pTimeScaleProxy )
  	:	BaseClass( pParent, "TimeScaleSettings", "#Replay_TimeScale" ),
  		m_pTimeScaleSlider( NULL ),
  		m_pTimeScaleProxy( pTimeScaleProxy )
  	{
  	}
  
  	virtual const char *GetResFile()
  	{
  		return "resource/ui/replayperformanceeditor/settings_timescale.res";
  	}
  
  	virtual void AddControls()
  	{
  		m_pTimeScaleSlider = dynamic_cast< Slider * >( FindChildByName( "TimeScaleSlider" ) );
  
  		AddSliderToLayout( SLIDER_TIMESCALE, m_pTimeScaleSlider, "#Replay_Scale", TIMESCALE_MIN, TIMESCALE_MAX, *m_pTimeScaleProxy );
  	}
  
  	enum FreeCamSliders_t
  	{
  		SLIDER_TIMESCALE,
  	};
  
  	Slider		*m_pTimeScaleSlider;
  	float		*m_pTimeScaleProxy;
  };
  
  //-----------------------------------------------------------------------------
  class CCameraOptionsPanel_Free : public CCameraOptionsPanel
  {
  	DECLARE_CLASS_SIMPLE( CCameraOptionsPanel_Free, CCameraOptionsPanel );
  public:
  	CCameraOptionsPanel_Free( Panel *pParent )
  	:	BaseClass( pParent, "FreeCameraSettings", "#Replay_FreeCam" ),
  		m_pAccelSlider( NULL ),
  		m_pSpeedSlider( NULL ),
  		m_pFovSlider( NULL ),
  		m_pRotFilterSlider( NULL ),
  		m_pShakeSpeedSlider( NULL ),
  		m_pShakeAmountSlider( NULL )
  	{
  	}
  
  	virtual const char *GetResFile()
  	{
  		return "resource/ui/replayperformanceeditor/camsettings_free.res";
  	}
  
  	virtual void AddControls()
  	{
  		m_pAccelSlider = dynamic_cast< Slider * >( FindChildByName( "AccelSlider" ) );
  		m_pSpeedSlider = dynamic_cast< Slider * >( FindChildByName( "SpeedSlider" ) );
  		m_pFovSlider = dynamic_cast< Slider * >( FindChildByName( "FovSlider" ) );
  		m_pRotFilterSlider = dynamic_cast< Slider * >( FindChildByName( "RotFilterSlider" ) );
  		m_pShakeSpeedSlider = dynamic_cast< Slider * >( FindChildByName( "ShakeSpeedSlider" ) );
  		m_pShakeAmountSlider = dynamic_cast< Slider * >( FindChildByName( "ShakeAmountSlider" ) );
  		m_pShakeDirSlider = dynamic_cast< Slider * >( FindChildByName( "ShakeDirSlider" ) );
  
  		AddSliderToLayout( SLIDER_ACCEL, m_pAccelSlider, "#Replay_Accel", FREE_CAM_ACCEL_MIN, FREE_CAM_ACCEL_MAX, ReplayCamera()->m_flRoamingAccel );
  		AddSliderToLayout( SLIDER_SPEED, m_pSpeedSlider, "#Replay_Speed", FREE_CAM_SPEED_MIN, FREE_CAM_SPEED_MAX, ReplayCamera()->m_flRoamingSpeed );
  		AddSliderToLayout( SLIDER_FOV, m_pFovSlider, "#Replay_Fov", FREE_CAM_FOV_MIN, FREE_CAM_FOV_MAX, ReplayCamera()->m_flRoamingFov[1] );
  		AddSliderToLayout( SLIDER_ROTFILTER, m_pRotFilterSlider, "#Replay_RotFilter", FREE_CAM_ROT_FILTER_MIN, FREE_CAM_ROT_FILTER_MAX, ReplayCamera()->m_flRoamingRotFilterFactor );
  		AddSliderToLayout( SLIDER_SHAKE_SPEED, m_pShakeSpeedSlider, "#Replay_ShakeSpeed", FREE_CAM_SHAKE_SPEED_MIN, FREE_CAM_SHAKE_SPEED_MAX, ReplayCamera()->m_flRoamingShakeSpeed );
  		AddSliderToLayout( SLIDER_SHAKE_AMOUNT, m_pShakeAmountSlider, "#Replay_ShakeAmount", FREE_CAM_SHAKE_AMOUNT_MIN, FREE_CAM_SHAKE_AMOUNT_MAX, ReplayCamera()->m_flRoamingShakeAmount );
  		AddSliderToLayout( SLIDER_SHAKE_DIR, m_pShakeDirSlider, "#Replay_ShakeDir", FREE_CAM_SHAKE_DIR_MIN, FREE_CAM_SHAKE_DIR_MAX, ReplayCamera()->m_flRoamingShakeDir );
  	}
  
  	enum FreeCamSliders_t
  	{
  		SLIDER_ACCEL,
  		SLIDER_SPEED,
  		SLIDER_FOV,
  		SLIDER_ROTFILTER,
  		SLIDER_SHAKE_SPEED,
  		SLIDER_SHAKE_AMOUNT,
  		SLIDER_SHAKE_DIR,
  	};
  
  	Slider		*m_pAccelSlider;
  	Slider		*m_pSpeedSlider;
  	Slider		*m_pFovSlider;
  	Slider		*m_pRotFilterSlider;
  	Slider		*m_pShakeSpeedSlider;
  	Slider		*m_pShakeAmountSlider;
  	Slider		*m_pShakeDirSlider;
  };
  
  //-----------------------------------------------------------------------------
  
  class CReplayButton : public CExImageButton
  {
  	DECLARE_CLASS_SIMPLE( CReplayButton, CExImageButton );
  public:
  	CReplayButton( Panel *pParent, const char *pName, const char *pText )
  	:	BaseClass( pParent, pName, pText ),
  		m_pTipText( NULL )
  	{
  	}
  
  	virtual void ApplySettings( KeyValues *pInResourceData )
  	{
  		BaseClass::ApplySettings( pInResourceData );
  
  		const char *pTipName = pInResourceData->GetString( "tipname" );
  		if ( pTipName && pTipName[0] )
  		{
  			const wchar_t *pTipText = g_pVGuiLocalize->Find( pTipName );
  			if ( pTipText && pTipText[0] )
  			{
  				const int nTipLength = V_wcslen( pTipText );
  				m_pTipText = new wchar_t[ nTipLength + 1 ];
  				V_wcsncpy( m_pTipText, pTipText, sizeof(wchar_t) * ( nTipLength + 1 ) );
  				m_pTipText[ nTipLength ] = L'\0';
  			}
  		}
  	}
  
  	virtual void OnCursorEntered()
  	{
  		BaseClass::OnCursorEntered();
  
  		CReplayPerformanceEditorPanel *pEditor = ReplayUI_GetPerformanceEditor();
  		if ( pEditor && m_pTipText )
  		{
  			pEditor->SetButtonTip( m_pTipText, this );
  			pEditor->ShowButtonTip( true );
  		}
  	}
  
  	virtual void OnCursorExited()
  	{
  		BaseClass::OnCursorExited();
  
  		CReplayPerformanceEditorPanel *pEditor = ReplayUI_GetPerformanceEditor();
  		if ( pEditor && m_pTipText )
  		{
  			pEditor->ShowButtonTip( false );
  		}
  	}
  
  private:
  	wchar_t	*m_pTipText;
  };
  
  DECLARE_BUILD_FACTORY_DEFAULT_TEXT( CReplayButton, CExImageButton );
  
  //-----------------------------------------------------------------------------
  
  #define MAX_FF_RAMP_TIME		8.0f	// The amount of time until we ramp to max scale value.
  
  class CReplayEditorFastForwardButton : public CReplayButton
  {
  	DECLARE_CLASS_SIMPLE( CReplayEditorFastForwardButton, CReplayButton );
  public:
  	CReplayEditorFastForwardButton( Panel *pParent, const char *pName, const char *pText )
  	:	BaseClass( pParent, pName, pText ),
  		m_flPressTime( 0.0f )
  	{
  		m_pHostTimescale = cvar->FindVar( "host_timescale" );
  		AssertMsg( m_pHostTimescale, "host_timescale lookup failed!" );
  
  		ivgui()->AddTickSignal( GetVPanel(), 10 );
  	}
  
  	~CReplayEditorFastForwardButton()
  	{
  		ivgui()->RemoveTickSignal( GetVPanel() );
  
  		// Avoid a non-1.0 host_timescale after replay edit, which can happen if
  		// the user is still holding downt he FF button at the end of the replay.
  		if ( m_pHostTimescale )
  		{
  			m_pHostTimescale->SetValue( 1.0f );
  		}
  
  		// Resume demo playback so that any demo played later won't start paused.
  		PlayDemo();
  	}
  
  	virtual void OnMousePressed( MouseCode code )
  	{
  		m_flPressTime = gpGlobals->realtime;
  		PlayDemo();
  
  		BaseClass::OnMousePressed( code );
  	}
  
  	virtual void OnMouseReleased( MouseCode code )
  	{
  		m_flPressTime = 0.0f;
  		PauseDemo();
  
  		BaseClass::OnMouseReleased( code );
  	}
  
  	void OnTick()
  	{
  		float flScale;
  		
  		if ( m_flPressTime == 0.0f )
  		{
  			flScale = 1.0f;
  		}
  		else
  		{
  			const float flElapsed = clamp( gpGlobals->realtime - m_flPressTime, 0.0f, MAX_FF_RAMP_TIME );
  			const float t = CubicEaseIn( flElapsed / MAX_FF_RAMP_TIME );
  
  			// If a shift key is down...
  			if ( input()->IsKeyDown( KEY_LSHIFT ) || input()->IsKeyDown( KEY_RSHIFT ) )
  			{
  				// ...slow down host_timescale.
  				flScale = .1f + .4f * t;
  			}
  			// If alt key down...
  			else if ( input()->IsKeyDown( KEY_LALT ) || input()->IsKeyDown( KEY_RALT ) )
  			{
  				// ...FF very quickly, ramp from 5 to 10.
  				flScale = 5.0f + 5.0f * t;
  			}
  			else
  			{
  				// Otherwise, start at 1.5 and ramp upwards over time.
  				flScale = 1.5f + 3.5f * t;
  			}
  		}
  
  		// Set host_timescale.
  		if ( m_pHostTimescale )
  		{
  			m_pHostTimescale->SetValue( flScale );
  		}
  	}
  
  private:
  	float	m_flPressTime;
  	ConVar *m_pHostTimescale;
  };
  
  DECLARE_BUILD_FACTORY_DEFAULT_TEXT( CReplayEditorFastForwardButton, CExImageButton );
  
  //-----------------------------------------------------------------------------
  
  class CRecLightPanel : public EditablePanel
  {
  	DECLARE_CLASS_SIMPLE( CRecLightPanel, vgui::EditablePanel );
  public:
  	CRecLightPanel( Panel *pParent )
  	:	EditablePanel( pParent, "RecLightPanel" ),
  		m_flPlayPauseTime( 0.0f ),
  		m_bPaused( false ),
  		m_bPerforming( false )
  	{
  		m_pRecLights[ 0 ] = NULL;
  		m_pRecLights[ 1 ] = NULL;
  		m_pPlayPause[ 0 ] = NULL;
  		m_pPlayPause[ 1 ] = NULL;
  	}
  
  	virtual void ApplySchemeSettings( IScheme *pScheme )
  	{
  		BaseClass::ApplySchemeSettings( pScheme );
  
  		LoadControlSettings( "resource/ui/replayperformanceeditor/reclight.res", "GAME" );
  
  		m_pRecLights[ 0 ] = dynamic_cast< ImagePanel * >( FindChildByName( "RecLightOffImg" ) );
  		m_pRecLights[ 1 ] = dynamic_cast< ImagePanel * >( FindChildByName( "RecLightOnImg" ) );
  
  		m_pPlayPause[ 0 ] = dynamic_cast< ImagePanel * >( FindChildByName( "PlayImg" ) );
  		m_pPlayPause[ 1 ] = dynamic_cast< ImagePanel * >( FindChildByName( "PauseImg" ) );
  
  		m_pCameraFringe = dynamic_cast< ImagePanel *>( FindChildByName( "CameraFringe" ) );
  		m_pCameraCrosshair = dynamic_cast< ImagePanel *>( FindChildByName( "CameraCrosshair" ) );
  	}
  	
  	virtual void PerformLayout()
  	{
  		BaseClass::PerformLayout();
  
  		SetVisible( m_bPerforming );
  
  		const int nScreenWidth = ScreenWidth();
  		const int nRecLightW = m_pRecLights[ 0 ]->GetWide();
  		int nXPos = nScreenWidth - nRecLightW + XRES( 6 );
  		int nYPos = -YRES( 8 );
  		m_pRecLights[ 0 ]->SetPos( nXPos, nYPos );
  		m_pRecLights[ 1 ]->SetPos( nXPos, nYPos );
  
  		const int nWidth = GetWide();
  		const int nHeight = GetTall();
  
  		// Setup camera fringe height
  		if ( m_pCameraFringe )
  		{
  			m_pCameraFringe->SetSize( nWidth, nHeight );
  			m_pCameraFringe->InstallMouseHandler( this );
  		}
  
  		// Setup camera cross hair height
  		if ( m_pCameraCrosshair )
  		{
  			int aImageSize[2];
  			IImage *pImage = m_pCameraCrosshair->GetImage();
  			pImage->GetSize( aImageSize[0], aImageSize[1] );
  
  			aImageSize[0] = m_pCameraCrosshair->GetWide();
  			aImageSize[1] = m_pCameraCrosshair->GetTall();
  
  			const int nStartY = YRES( 13 );
  
  			m_pCameraCrosshair->SetBounds(
  				nStartY + ( nWidth - aImageSize[0] ) / 2,
  				nStartY + ( nHeight - aImageSize[1] ) / 2,
  				aImageSize[0] - 2 * nStartY,
  				aImageSize[1] - 2 * nStartY
  			);
  
  			m_pCameraCrosshair->InstallMouseHandler( this );
  		}
  	}
  
  	void UpdateBackgroundVisibility()
  	{
  		m_pCameraCrosshair->SetVisible( m_bPaused );
  		m_pCameraFringe->SetVisible( m_bPaused );
  	}
  
  	virtual void OnThink()
  	{
  		const float flTime = gpGlobals->realtime;
  		bool bPauseAnimating = m_flPlayPauseTime > 0.0f &&
  			                   flTime >= m_flPlayPauseTime &&
  							   flTime < ( m_flPlayPauseTime + m_flAnimTime );
  
  		// Setup light visibility
  		int nOnOff = fmod( flTime * 2.0f, 2.0f );
  		bool bOnLightVisible = (bool)nOnOff;
  		bool bRecording = g_pReplayPerformanceController->IsRecording();
  		m_pRecLights[ 0 ]->SetVisible( m_bPaused || ( bRecording && !bOnLightVisible ) );
  		m_pRecLights[ 1 ]->SetVisible( bRecording && ( !m_bPaused && bOnLightVisible ) );
  
  		// Deal with fringe and crosshair vis
  		UpdateBackgroundVisibility();
  
  		int iPlayPauseActive = (int)m_bPaused;
  
  		// Animate the pause icon
  		if ( bPauseAnimating )
  		{
  			const float t = clamp( ( flTime - m_flPlayPauseTime ) / m_flAnimTime, 0.0f, 1.0f );
  			const float s = SCurve( t );
  			const int nSize = (int)Lerp( s, 60.0f, 60.0f * m_nAnimScale );
  			int aCrossHairPos[2];
  			m_pCameraCrosshair->GetPos( aCrossHairPos[0], aCrossHairPos[1] );
  			const int nScreenXCenter = aCrossHairPos[0] + m_pCameraCrosshair->GetWide() / 2;
  			const int nScreenYCenter = aCrossHairPos[1] + m_pCameraCrosshair->GetTall() / 2;
  
  			m_pPlayPause[ iPlayPauseActive ]->SetBounds( 
  				nScreenXCenter - nSize / 2,
  				nScreenYCenter - nSize / 2,
  				nSize,
  				nSize
  			);
  
  			m_pPlayPause[ iPlayPauseActive ]->SetAlpha( (int)( MIN( 0.5f, 1.0f - s ) * 255) );
  		}
  
  		m_pPlayPause[  iPlayPauseActive ]->SetVisible( bPauseAnimating );
  		m_pPlayPause[ !iPlayPauseActive ]->SetVisible( false );
  	}
  
  	void UpdatePauseState( bool bPaused )
  	{
  		if ( bPaused == m_bPaused )
  			return;
  
  		m_bPaused = bPaused;
  
  		m_flPlayPauseTime = gpGlobals->realtime;
  	}
  
  	void SetPerforming( bool bPerforming )
  	{
  		if ( bPerforming == m_bPerforming )
  			return;
  
  		m_bPerforming = bPerforming;
  		InvalidateLayout( true, false );
  	}
  
  	float		m_flPlayPauseTime;
  	bool		m_bPaused;
  	bool		m_bPerforming;
  	ImagePanel	*m_pPlayPause[2];	// 0=play, 1=pause
  	ImagePanel	*m_pRecLights[2];	// 0=off, 1=on
  	ImagePanel	*m_pCameraFringe;
  	ImagePanel	*m_pCameraCrosshair;
  
  	CPanelAnimationVar( int, m_nAnimScale, "anim_scale", "4" );
  	CPanelAnimationVar( float, m_flAnimTime, "anim_time", "1.5" );
  };
  
  //-----------------------------------------------------------------------------
  
  CReplayPerformanceEditorPanel::CReplayPerformanceEditorPanel( Panel *parent, ReplayHandle_t hReplay )
  :	EditablePanel( parent, "ReplayPerformanceEditor" ),
  	m_hReplay( hReplay ),
  	m_flLastTime( -1 ),
  	m_nRedBlueLabelRightX( 0 ),
  	m_nBottomPanelStartY( 0 ),
  	m_nBottomPanelHeight( 0 ),
  	m_nLastRoundedTime( -1 ),
  	m_flSpaceDownStart( 0.0f ),
  	m_flOldFps( -1.0f ),
  	m_flLastTimeSpaceBarPressed( 0.0f ),
  	m_flActiveTimeInEditor( 0.0f ),
  	m_flTimeScaleProxy( 1.0f ),
  	m_iCameraSelection( CAM_FIRST ),
  	m_bMousePressed( false ),
  	m_bMouseDown( false ),
  	m_nMouseClickedOverCameraSettingsPanel( CAM_INVALID ),
  	m_bShownAtLeastOnce( false ),
  	m_bAchievementAwarded( false ),
  	m_pImageList( NULL ),
  	m_pCurTimeLabel( NULL ),
  	m_pTotalTimeLabel( NULL ),
  	m_pPlayerNameLabel( NULL ),
  	m_pMouseTargetPanel( NULL ),
  	m_pSlowMoButton( NULL ),
  	m_pRecLightPanel( NULL ),
  	m_pPlayerCellData( NULL ),
  	m_pBottom( NULL ),
  	m_pMenuButton( NULL ),
  	m_pMenu( NULL ),
  	m_pPlayerCellsPanel( NULL ),
  	m_pButtonTip( NULL ),
  	m_pSavingDlg( NULL )
  {
  	V_memset( m_pCameraButtons, 0, sizeof( m_pCameraButtons ) );
  	V_memset( m_pCtrlButtons, 0, sizeof( m_pCtrlButtons ) );
  	V_memset( m_pCameraOptionsPanels, NULL, sizeof( m_pCameraOptionsPanels ) );
  
  	m_pCameraOptionsPanels[ CAM_FREE ] = new CCameraOptionsPanel_Free( this );
  	m_pCameraOptionsPanels[ COMPONENT_TIMESCALE ] = new CTimeScaleOptionsPanel( this, &m_flTimeScaleProxy );
  
  	m_nRedBlueSigns[0] = -1;
  	m_nRedBlueSigns[1] = 1;
  	m_iCurPlayerTarget = -1;
  	m_bCurrentTargetNeedsVisibilityUpdate = false;
  
  	m_pImageList = new ImageList( false );
  
  	SetParent( g_pClientMode->GetViewport() );
  
  	HScheme hScheme = scheme()->LoadSchemeFromFileEx( enginevgui->GetPanel( PANEL_CLIENTDLL ), "resource/ClientScheme.res", "ClientScheme" );
  	SetScheme( hScheme );
  
  	ivgui()->AddTickSignal( GetVPanel(), 16 );	// Roughly 60hz
  
  	MakePopup( true );
  	SetMouseInputEnabled( true );
  
  	// Create bottom
  	m_pBottom = new EditablePanel( this, "BottomPanel" );
  
  	// Add player cells
  	m_pPlayerCellsPanel = new EditablePanel( m_pBottom, "PlayerCellsPanel" );
  	for ( int i = 0; i < 2; ++i )
  	{
  		for ( int j = 0; j <= MAX_PLAYERS; ++j )
  		{
  			m_pPlayerCells[i][j] = new CPlayerCell( m_pPlayerCellsPanel, "PlayerCell", &m_iCurPlayerTarget );
  			m_pPlayerCells[i][j]->SetVisible( false );
  			AddPanelKeyboardInputDisableList( m_pPlayerCells[i][j] );
  		}
  	}
  
  	// Create rec light panel
  	m_pRecLightPanel = SETUP_PANEL( new CRecLightPanel( g_pClientMode->GetViewport() ) );
  
  	// Display "enter performance mode" tip
  	DisplayPerformanceTip( "#Replay_PerfTip_EnterPerfMode", &replay_perftip_count_enter, MAX_TIP_DISPLAYS );
  
  	// Create menu
  	m_pMenu = new Menu( this, "Menu" );
  	m_aMenuItemIds[ MENU_SAVE   ] = m_pMenu->AddMenuItem( "#Replay_Save", "menu_save", this );
  	m_aMenuItemIds[ MENU_SAVEAS ] = m_pMenu->AddMenuItem( "#Replay_SaveAs", "menu_saveas", this );
  	m_pMenu->AddSeparator();
  	m_aMenuItemIds[ MENU_EXIT   ] = m_pMenu->AddMenuItem( "#Replay_Exit", "menu_exit", this );
  
  	m_pMenu->EnableUseMenuManager( false );	// The menu manager doesn't play nice with the menu button
  }
  
  CReplayPerformanceEditorPanel::~CReplayPerformanceEditorPanel()
  {
  	m_pRecLightPanel->MarkForDeletion();
  	m_pRecLightPanel = NULL;
  
  	m_pButtonTip->MarkForDeletion();
  	m_pButtonTip = NULL;
  
  	g_bIsReplayRewinding = false;
  
  	surface()->PlaySound( "replay\\performanceeditorclosed.wav" );
  
  	CPerformanceTip::Cleanup();
  
  	ClearPlayerCellData();
  }
  
  void CReplayPerformanceEditorPanel::ClearPlayerCellData()
  {
  	if ( m_pPlayerCellData )
  	{
  		m_pPlayerCellData->deleteThis();
  		m_pPlayerCellData = NULL;
  	}
  }
  
  void CReplayPerformanceEditorPanel::AddPanelKeyboardInputDisableList( Panel *pPanel )
  {
  	m_lstDisableKeyboardInputPanels.AddToTail( pPanel );
  }
  
  void CReplayPerformanceEditorPanel::ApplySchemeSettings( IScheme *pScheme )
  {
  	BaseClass::ApplySchemeSettings( pScheme );
  
  	LoadControlSettings( "resource/ui/replayperformanceeditor/main.res", "GAME" );
  
  	m_lstDisableKeyboardInputPanels.RemoveAll();
  
  	int nParentWidth = GetParent()->GetWide();
  	int nParentHeight = GetParent()->GetTall();
  
  	// Set size of this panel
  	SetSize( nParentWidth, nParentHeight );
  
  	// Layout bottom
  	if ( m_pBottom )
  	{
  		m_nBottomPanelHeight = m_pBottom->GetTall();	// Get from .res
  		m_nBottomPanelStartY = nParentHeight - m_nBottomPanelHeight;
  		m_pBottom->SetBounds( 0, m_nBottomPanelStartY, nParentWidth, m_nBottomPanelHeight );
  	}
  
  	// Layout rec light panel - don't overlap bottom panel
  	m_pRecLightPanel->SetBounds( 0, 0, ScreenWidth(), m_nBottomPanelStartY );
  
  	// Setup camera buttons
  	const int nNumCameraButtons = NCAMS;
  	const char *pCameraButtonNames[nNumCameraButtons] = { "CameraFree", "CameraThird", "CameraFirst", "TimeScaleButton" };
  	int nCurButtonX = nParentWidth - m_nRightMarginWidth;
  	int nLeftmostCameraButtonX = 0;
  	for ( int i = 0; i < nNumCameraButtons; ++i )
  	{
  		m_pCameraButtons[i] = dynamic_cast< CExImageButton * >( FindChildByName( pCameraButtonNames[ i ] ) );
  		if ( m_pCameraButtons[i] )
  		{
  			CExImageButton *pCurButton = m_pCameraButtons[ i ];
  			if ( !pCurButton )
  				continue;
  
  			nCurButtonX -= pCurButton->GetWide();
  
  			int nX, nY;
  			pCurButton->GetPos( nX, nY );
  			pCurButton->SetPos( nCurButtonX, nY );
  
  			pCurButton->SetParent( m_pBottom );
  			pCurButton->AddActionSignalTarget( this );
  
  #if !defined( TF_CLIENT_DLL )
  			pCurButton->SetPaintBorderEnabled( false );
  #endif
  
  			AddPanelKeyboardInputDisableList( pCurButton );
  		}
  	}
  	nLeftmostCameraButtonX = nCurButtonX;
  
  	static const char *s_pControlButtonNames[NUM_CTRLBUTTONS] = {
  		"InButton", "GotoBeginningButton", "RewindButton",
  		"PlayButton",
  		"FastForwardButton", "GotoEndButton", "OutButton"
  	};
  	for ( int i = 0; i < NUM_CTRLBUTTONS; ++i )
  	{
  		CExImageButton *pCurButton = dynamic_cast< CExImageButton * >( FindChildByName( s_pControlButtonNames[ i ] ) );	Assert( pCurButton );
  		if ( !pCurButton )
  			continue;
  
  		pCurButton->SetParent( m_pBottom );
  		pCurButton->AddActionSignalTarget( this );
  
  		AddPanelKeyboardInputDisableList( pCurButton );
  
  #if !defined( TF_CLIENT_DLL )
  		pCurButton->SetPaintBorderEnabled( false );
  #endif
  
  		m_pCtrlButtons[ i ] = pCurButton;
  	}
  
  	// If the performance in tick is set, highlight the in point button
  	{
  		CReplayPerformance *pSavedPerformance = GetSavedPerformance();
  		m_pCtrlButtons[ CTRLBUTTON_IN  ]->SetSelected( pSavedPerformance && pSavedPerformance->HasInTick() );
  		m_pCtrlButtons[ CTRLBUTTON_OUT ]->SetSelected( pSavedPerformance && pSavedPerformance->HasOutTick() );
  	}
  
  	// Select first-person camera by default.
  	UpdateCameraSelectionPosition( CAM_FIRST );
  
  	// Position time label
  	m_pCurTimeLabel = dynamic_cast< CExLabel * >( FindChildByName( "CurTimeLabel" ) );
  	m_pTotalTimeLabel = dynamic_cast< CExLabel * >( FindChildByName( "TotalTimeLabel" ) );
  
  	m_pCurTimeLabel->SetParent( m_pBottom );
  	m_pTotalTimeLabel->SetParent( m_pBottom );
  
  	// Get player name label
  	m_pPlayerNameLabel = dynamic_cast< CExLabel * >( FindChildByName( "PlayerNameLabel" ) );
  
  	// Get mouse target panel
  	m_pMouseTargetPanel = dynamic_cast< EditablePanel * >( FindChildByName( "MouseTargetPanel" ) );
  
  	for ( int i = 0; i < 2; ++i )
  	{
  		for ( int j = 0; j <= MAX_PLAYERS; ++j )
  		{
  			m_pPlayerCells[i][j]->SetMouseInputEnabled( true );
  		}
  	}
  
  	// Get menu button
  	m_pMenuButton = dynamic_cast< CExImageButton * >( FindChildByName( "MenuButton" ) );
  	AddPanelKeyboardInputDisableList( m_pMenuButton );
  	m_pMenuButton->SetMouseInputEnabled( true );
  #if !defined( TF_CLIENT_DLL )
  	m_pMenuButton->SetPaintBorderEnabled( false );
  #endif
  
  	// Get button tip
  	m_pButtonTip = dynamic_cast< CReplayTipLabel * >( FindChildByName( "ButtonTip" ) );
  	m_pButtonTip->SetParent( g_pClientMode->GetViewport() );
  }
  
  static void Replay_GotoTick( bool bConfirmed, void *pContext )
  {
  	if ( bConfirmed )
  	{
  		int nGotoTick = (int)pContext;
  		CFmtStr fmtCmd( "demo_gototick %i\ndemo_pause\n", nGotoTick );
  		engine->ClientCmd_Unrestricted( fmtCmd.Access() );
  	}
  }
  
  void CReplayPerformanceEditorPanel::OnSliderMoved( KeyValues *pParams )
  {
  }
  
  void CReplayPerformanceEditorPanel::OnInGameMouseWheelEvent( int nDelta )
  {
  	HandleMouseWheel( nDelta );
  }
  
  void CReplayPerformanceEditorPanel::HandleMouseWheel( int nDelta )
  {
  	if ( ReplayCamera()->GetMode() == OBS_MODE_ROAMING )
  	{
  		// Invert mousewheel input if necessary
  		if ( replay_editor_fov_mousewheel_invert.GetBool() )
  		{
  			nDelta *= -1;
  		}
  
  		float &flFov = ReplayCamera()->m_flRoamingFov[1];
  		flFov = clamp( flFov - nDelta * replay_editor_fov_mousewheel_multiplier.GetFloat(), FREE_CAM_FOV_MIN, FREE_CAM_FOV_MAX );
  
  		// Update FOV slider in free camera settings
  		CCameraOptionsPanel_Free *pFreeCamOptions = static_cast< CCameraOptionsPanel_Free * >( m_pCameraOptionsPanels[ CAM_FREE ] );
  		pFreeCamOptions->m_pFovSlider->SetValue( flFov - FREE_CAM_FOV_MIN, false );
  	}
  }
  
  void CReplayPerformanceEditorPanel::ApplySettings( KeyValues *pInResourceData )
  {
  	BaseClass::ApplySettings( pInResourceData );
  
  	ClearPlayerCellData();
  
  	KeyValues *pPlayerCellData = pInResourceData->FindKey( "PlayerCell" );
  	if ( pPlayerCellData )
  	{
  		m_pPlayerCellData = new KeyValues( "PlayerCell" );
  		pPlayerCellData->CopySubkeys( m_pPlayerCellData );
  	}
  }
  
  CameraMode_t CReplayPerformanceEditorPanel::IsMouseOverActiveCameraOptionsPanel( int nMouseX, int nMouseY )
  {
  	// In one of the camera options panels?
  	for ( int i = 0; i < NCAMS; ++i )
  	{
  		CCameraOptionsPanel *pCurPanel = m_pCameraOptionsPanels[ i ];
  		if ( pCurPanel && pCurPanel->IsVisible() && pCurPanel->IsWithin( nMouseX, nMouseY ) )
  			return (CameraMode_t)i;
  	}
  
  	return CAM_INVALID;
  }
  
  void CReplayPerformanceEditorPanel::OnMouseWheeled( int nDelta )
  {
  	HandleMouseWheel( nDelta );
  }
  
  void CReplayPerformanceEditorPanel::OnTick()
  {
  	BaseClass::OnTick();
  
  //	engine->Con_NPrintf( 0, "timescale: %f", g_pReplayPerformanceController->GetPlaybackTimeScale() );
  
  	C_ReplayCamera *pCamera = ReplayCamera();
  	if ( !pCamera )
  		return;
  
  	// Calc elapsed time
  	float flElapsed = gpGlobals->realtime - m_flLastTime;
  	m_flLastTime = gpGlobals->realtime;
  
  	// If this is the first time we're running and camera is valid, get primary target
  	if ( m_iCurPlayerTarget < 0 )
  	{
  		m_iCurPlayerTarget = pCamera->GetPrimaryTargetIndex();
  	}
  
  	// NOTE: Third-person is not "controllable" yet
  	int nCameraMode = pCamera->GetMode();
  	bool bInAControllableCameraMode = nCameraMode == OBS_MODE_ROAMING || nCameraMode == OBS_MODE_CHASE;
  
  	// Get mouse cursor pos
  	int nMouseX, nMouseY;
  	input()->GetCursorPos( nMouseX, nMouseY );
  
  	// Toggle in and out of camera control if appropriate
  	// Mouse pressed?
  	bool bMouseDown = input()->IsMouseDown( MOUSE_LEFT );
  	m_bMousePressed = bMouseDown && !m_bMouseDown;
  	m_bMouseDown = bMouseDown;
  
  	// Reset this flag if mouse is no longer down
  	if ( !m_bMouseDown )
  	{
  		m_nMouseClickedOverCameraSettingsPanel = CAM_INVALID;
  	}
  
  	bool bNoDialogsUp = TFModalStack()->IsEmpty();
  	bool bMouseCursorOverPerfEditor = nMouseY >= m_nBottomPanelStartY;
  	bool bMouseOverMenuButton = m_pMenuButton->IsWithin( nMouseX, nMouseY );
  	bool bMouseOverMenu = m_pMenu->IsWithin( nMouseX, nMouseY );
  	bool bRecording = g_pReplayPerformanceController->IsRecording();
  	if ( IsVisible() && m_bMousePressed )
  	{
  		CameraMode_t nActiveOptionsPanel = IsMouseOverActiveCameraOptionsPanel( nMouseX, nMouseY );
  		if ( nActiveOptionsPanel != CAM_INVALID )
  		{
  			m_nMouseClickedOverCameraSettingsPanel = nActiveOptionsPanel;
  		}
  		else if ( m_pMenu->IsVisible() && !m_pMenu->IsWithin( nMouseX, nMouseY ) )
  		{
  			ToggleMenu();
  		}
  		else if ( bInAControllableCameraMode && !bMouseCursorOverPerfEditor && !bMouseOverMenuButton &&
  			!bMouseOverMenu && bNoDialogsUp )
  		{
  			if ( bRecording )
  			{
  				bool bMouseInputEnabled = IsMouseInputEnabled();
  
  				// Already in a controllable camera mode?
  				if ( bMouseInputEnabled )
  				{
  					DisplayPerformanceTip( "#Replay_PerfTip_ExitFreeCam", &replay_perftip_count_freecam_exit, MAX_TIP_DISPLAYS );	
  					surface()->PlaySound( "replay\\cameracontrolmodeentered.wav" );
  				}
  				else
  				{
  					DisplayPerformanceTip( "#Replay_PerfTip_EnterFreeCam", &replay_perftip_count_freecam_enter, MAX_TIP_DISPLAYS );
  					surface()->PlaySound( "replay\\cameracontrolmodeexited.wav" );
  				}
  
  				SetMouseInputEnabled( !bMouseInputEnabled );
  			}
  			else
  			{
  				// Play an error sound
  				surface()->PlaySound( "replay\\cameracontrolerror.wav" );
  			}
  		}
  	}
  
  	// Show panel if space key bar is down
  	bool bSpaceDown = bNoDialogsUp && !enginevgui->IsGameUIVisible() && input()->IsKeyDown( KEY_SPACE );
  	m_bSpacePressed = bSpaceDown && !m_bSpaceDown;
  	m_bSpaceDown = bSpaceDown;
  
  	// Modify visibility?
  	bool bShow = IsVisible();
  	if ( m_bSpacePressed )
  	{
  		bShow = !IsVisible();
  	}
  
  	// Set visibility?
  	if ( IsVisible() != bShow )
  	{
  		ShowPanel( bShow );
  		m_bShownAtLeastOnce = true;
  
  		// For achievements:
  		Achievements_OnSpaceBarPressed();
  	}
  
  	// Factor in host_timescale.
  	float flScaledElapsed = flElapsed;
  	ConVarRef host_timescale( "host_timescale" );
  	if ( host_timescale.GetFloat() > 0 )
  	{
  		flScaledElapsed *= host_timescale.GetFloat();
  	}
  
  	// Do FOV smoothing
  	ReplayCamera()->SmoothFov( flScaledElapsed );
  
  	// Don't do any more processing if not needed
  	if ( !m_bShownAtLeastOnce )
  		return;
  
  	// Update time text if necessary
  	UpdateTimeLabels();
  
  	// Make all player cells invisible
  	int nTeamCounts[2] = {0,0};
  	int nCurTeam = 0;
  	for ( int i = 0; i < 2; ++i )
  	for ( int j = 0; j <= MAX_PLAYERS; ++j )
  	{
  		m_pPlayerCells[i][j]->SetVisible( false );
  	}
  
  	int iMouseOverPlayerIndex = -1;
  	CPlayerCell *pMouseOverCell = NULL;
  
  	// Update player cells
  	bool bLayoutPlayerCells = true;	// TODO: only layout when necessary
  	C_ReplayGame_PlayerResource_t *pGamePlayerResource = dynamic_cast< C_ReplayGame_PlayerResource_t * >( g_PR );
  	for ( int iPlayer = 1; iPlayer <= MAX_PLAYERS; ++iPlayer )
  	{
  		IGameResources *pGR = GameResources();
  
  		if ( !pGR || !pGR->IsConnected( iPlayer ) )
  			continue;
  
  		// Which team?
  		int iTeam = pGR->GetTeam( iPlayer );
  		switch ( iTeam )
  		{
  		case REPLAY_TEAM_TEAM0:
  			++nTeamCounts[0];
  			nCurTeam = 0;
  			break;
  		case REPLAY_TEAM_TEAM1:
  			++nTeamCounts[1];
  			nCurTeam = 1;
  			break;
  		default:
  			nCurTeam = -1;
  			break;
  		}
  
  		if ( nCurTeam < 0 )
  			continue;
  
  #if !defined( CSTRIKE_DLL )
  		int iPlayerClass = pGamePlayerResource->GetPlayerClass( iPlayer );
  		if ( iPlayerClass == REPLAY_CLASS_UNDEFINED )
  			continue;
  #endif
  					 
  		int nCurTeamCount = nTeamCounts[ nCurTeam ];
  		CPlayerCell* pCell = m_pPlayerCells[ nCurTeam ][ nCurTeamCount-1 ];
  
  		// Cache the player index
  		pCell->m_iPlayerIndex = iPlayer;
  
  		// Make visible
  		pCell->SetVisible( true );
  
  		// Show leaderboard icon
  #if defined( TF_CLIENT_DLL )
  		char szClassImg[64];
  		extern const char *g_aPlayerClassNames_NonLocalized[ REPLAY_NUM_CLASSES ];
  		char const *pClassName = iPlayerClass == TF_CLASS_DEMOMAN
  			? "demo"
  			: g_aPlayerClassNames_NonLocalized[ iPlayerClass ];
  		V_snprintf( szClassImg, sizeof( szClassImg ), "../HUD/leaderboard_class_%s", pClassName );
  
  		// Show dead icon instead?
  		if ( !pGamePlayerResource->IsAlive( iPlayer ) )
  		{
  			V_strcat( szClassImg, "_d", sizeof( szClassImg ) );
  		}
  
  		IImage *pImage = scheme()->GetImage( szClassImg, true );
  		if ( pImage )
  		{
  			pImage->SetSize( 32, 32 );
  			pCell->GetImage()->SetImage( pImage );
  		}
  
  #elif defined( CSTRIKE_DLL )
  		// TODO - create and use class icons
  		char szText[16];
  		V_snprintf( szText, sizeof( szText ), "%i", nTeamCounts[ nCurTeam ] );
  		pCell->SetText( szText );
  #endif
  
  		// Display player name if mouse is over the current cell
  		if ( pCell->IsWithin( nMouseX, nMouseY ) )
  		{
  			iMouseOverPlayerIndex = iPlayer;
  			pMouseOverCell = pCell;
  		}
  	}
  
  	// Check to see if we're hovering over a camera-mode, and if so, display its options panel if it has one
  	if ( bRecording )
  	{
  		for ( int i = 0; i < NCAMS; ++i )
  		{
  			CCameraOptionsPanel *pCurOptionsPanel = m_pCameraOptionsPanels[ i ];
  			if ( !pCurOptionsPanel )
  				continue;
  
  			bool bMouseOverButton = m_pCameraButtons[ i ]->IsWithin( nMouseX, nMouseY );
  			bool bMouseOverOptionsPanel = pCurOptionsPanel->IsWithin( nMouseX, nMouseY );
  			bool bInCameraModeThatMouseIsOver = ReplayCamera()->GetMode() == GetCameraModeFromButtonIndex( (CameraMode_t)i );
  			bool bDontCareAboutCameraMode = i == COMPONENT_TIMESCALE;
  			bool bActivate = ( i == m_nMouseClickedOverCameraSettingsPanel ) ||
  				( ( ( bInCameraModeThatMouseIsOver || bDontCareAboutCameraMode ) && bMouseOverButton ) || ( bMouseOverOptionsPanel && pCurOptionsPanel->IsVisible() ) );
  			pCurOptionsPanel->SetVisible( bActivate );
  		}
  	}
  
  	if ( bLayoutPlayerCells )
  	{
  		LayoutPlayerCells();
  	}
  
  	// Setup player name label and temporary camera view
  	if ( m_pPlayerNameLabel && pGamePlayerResource && pMouseOverCell )
  	{
  		m_pPlayerNameLabel->SetText( pGamePlayerResource->GetPlayerName( iMouseOverPlayerIndex ) );
  		m_pPlayerNameLabel->SizeToContents();
  
  		int nCellPos[2];
  		pMouseOverCell->GetPos( nCellPos[0], nCellPos[1] );
  
  		int nLabelX = MAX(
  			nCellPos[0],
  			m_nRedBlueLabelRightX
  		);
  		int nLabelY = m_nBottomPanelStartY + ( m_nBottomPanelHeight - m_pPlayerNameLabel->GetTall() ) / 2;
  		m_pPlayerNameLabel->SetPos( nLabelX, nLabelY );
  
  		m_pPlayerNameLabel->SetVisible( true );
  
  		// Setup camera
  		pCamera->SetPrimaryTarget( iMouseOverPlayerIndex );
  	}
  	else
  	{
  		m_pPlayerNameLabel->SetVisible( false );
  
  		// Set camera to last valid target
  		Assert( m_iCurPlayerTarget >= 0 );
  		pCamera->SetPrimaryTarget( m_iCurPlayerTarget );
  	}
  
  	// If user clicked, assume it was the selected cell and set primary target in camera
  	if ( iMouseOverPlayerIndex >= 0 )
  	{
  		pCamera->SetPrimaryTarget( iMouseOverPlayerIndex );
  	}
  	else
  	{
  		pCamera->SetPrimaryTarget( m_iCurPlayerTarget );
  	}
  
  	// fixes a case where the replay would be paused and the player would cycle cameras but the 
  	// target's visibility wouldn't be updated until the replay was unpaused (they would be invisible)
  	if ( m_bCurrentTargetNeedsVisibilityUpdate )
  	{
  		C_BaseEntity *pTarget = ClientEntityList().GetEnt( pCamera->GetPrimaryTargetIndex() );
  		if ( pTarget )
  		{
  			pTarget->UpdateVisibility();
  		}
  
  		m_bCurrentTargetNeedsVisibilityUpdate = false;
  	}
  
  	// If in free-cam mode, add set view event if we're not paused
  	if ( bInAControllableCameraMode && m_bShownAtLeastOnce && bRecording )
  	{
  		AddSetViewEvent();
  		AddTimeScaleEvent( m_flTimeScaleProxy );
  	}
  
  	// Set paused state in rec light
  	const bool bPaused = IsPaused();
  	m_pRecLightPanel->UpdatePauseState( bPaused );
  
  	Achievements_Think( flElapsed );
  }
  
  void CReplayPerformanceEditorPanel::Achievements_OnSpaceBarPressed()
  {
  	m_flLastTimeSpaceBarPressed = gpGlobals->realtime;
  }
  
  void CReplayPerformanceEditorPanel::Achievements_Think( float flElapsed )
  {
  //	engine->Con_NPrintf( 10, "total time: %f", m_flActiveTimeInEditor );
  //	engine->Con_NPrintf( 11, "last time space bar pressed: %f", m_flLastTimeSpaceBarPressed );
  
  	// Already awarded one this editing session?
  	if ( m_bAchievementAwarded )
  		return;
  	
  	// Too much idle time since last activity?
  	if ( gpGlobals->realtime - m_flLastTimeSpaceBarPressed > 60.0f )
  	{
  		m_flActiveTimeInEditor = 0.0f;
  		return;
  	}
  
  	// Accumulate active time
  	m_flActiveTimeInEditor += flElapsed;
  
  	// Award now if three-minutes of non-idle time has passed
  	const float flMinutes = 60.0f * 3.0f;
  	if ( m_flActiveTimeInEditor < flMinutes )
  		return;
  
  	Achievements_Grant();
  }
  
  void CReplayPerformanceEditorPanel::Achievements_Grant()
  {
  #if defined( TF_CLIENT_DLL )
  	g_AchievementMgrTF.AwardAchievement( ACHIEVEMENT_TF_REPLAY_EDIT_TIME );
  #endif
  
  	// Awarded
  	m_bAchievementAwarded = true;
  }
  
  bool CReplayPerformanceEditorPanel::IsPaused()
  {
  	return IsVisible();
  }
  
  CReplayPerformance *CReplayPerformanceEditorPanel::GetPerformance() const
  {
  	return g_pReplayPerformanceController->GetPerformance();
  }
  
  CReplayPerformance *CReplayPerformanceEditorPanel::GetSavedPerformance() const
  {
  	return g_pReplayPerformanceController->GetSavedPerformance();
  }
  
  int CReplayPerformanceEditorPanel::GetCameraModeFromButtonIndex( CameraMode_t iCamera )
  {
  	switch ( iCamera )
  	{
  	case CAM_FREE:	return OBS_MODE_ROAMING;
  	case CAM_THIRD:	return OBS_MODE_CHASE;
  	case CAM_FIRST:	return OBS_MODE_IN_EYE;
  	}
  	return CAM_INVALID;
  }
  
  void CReplayPerformanceEditorPanel::UpdateTimeLabels()
  {
  	CReplay *pPlayingReplay = g_pReplayManager->GetPlayingReplay();
  
  	if ( !pPlayingReplay || !m_pCurTimeLabel || !m_pTotalTimeLabel )
  		return;
  
  	float flCurTime, flTotalTime;
  	g_pClientReplayContext->GetPlaybackTimes( flCurTime, flTotalTime, pPlayingReplay, GetPerformance() );
  
  	int nCurRoundedTime = (int)flCurTime;	// Essentially floor'd
  	if ( nCurRoundedTime == m_nLastRoundedTime )
  		return;
  
  	m_nLastRoundedTime = nCurRoundedTime;
  
  	// Set current time text
  	char szTimeText[64];
  	V_snprintf( szTimeText, sizeof( szTimeText ), "%s", CReplayTime::FormatTimeString( nCurRoundedTime ) );
  	m_pCurTimeLabel->SetText( szTimeText );
  
  	// Set total time text
  	V_snprintf( szTimeText, sizeof( szTimeText ), "%s", CReplayTime::FormatTimeString( (int)flTotalTime ) );
  	m_pTotalTimeLabel->SetText( szTimeText );
  
  	// Center between left-most camera button and play/pause button
  	m_pCurTimeLabel->SizeToContents();
  	m_pTotalTimeLabel->SizeToContents();
  }
  
  void CReplayPerformanceEditorPanel::UpdateCameraSelectionPosition( CameraMode_t nCameraMode )
  {
  	Assert( nCameraMode >= 0 && nCameraMode < NCAMS );
  	m_iCameraSelection = nCameraMode;
  
  	UpdateCameraButtonImages();
  }
  
  void CReplayPerformanceEditorPanel::UpdateFreeCamSettings( const SetViewParams_t &params )
  {
  	CCameraOptionsPanel_Free *pSettingsPanel = dynamic_cast< CCameraOptionsPanel_Free * >( m_pCameraOptionsPanels[ CAM_FREE ] );
  	if ( !pSettingsPanel )
  		return;
  
  	pSettingsPanel->SetValue( CCameraOptionsPanel_Free::SLIDER_ACCEL, params.m_flAccel );
  	pSettingsPanel->SetValue( CCameraOptionsPanel_Free::SLIDER_SPEED, params.m_flSpeed );
  	pSettingsPanel->SetValue( CCameraOptionsPanel_Free::SLIDER_FOV, params.m_flFov );
  	pSettingsPanel->SetValue( CCameraOptionsPanel_Free::SLIDER_ROTFILTER, params.m_flRotationFilter );
  }
  
  void CReplayPerformanceEditorPanel::UpdateTimeScale( float flScale )
  {
  	CTimeScaleOptionsPanel *pSettingsPanel = dynamic_cast< CTimeScaleOptionsPanel * >( m_pCameraOptionsPanels[ COMPONENT_TIMESCALE ] );
  	if ( !pSettingsPanel )
  		return;
  
  	pSettingsPanel->SetValue( CTimeScaleOptionsPanel::SLIDER_TIMESCALE, flScale );
  }
  
  void CReplayPerformanceEditorPanel::LayoutPlayerCells()
  {
  	int nPanelHeight = m_pPlayerCellsPanel->GetTall();
  	int nCellBuffer = XRES(1);
  	for ( int i = 0; i < 2; ++i )
  	{
  		int nCurX = m_nRedBlueLabelRightX;
  
  		for ( int j = 0; j <= MAX_PLAYERS; ++j )
  		{
  			CPlayerCell *pCurCell = m_pPlayerCells[i][j];
  			if ( !pCurCell->IsVisible() )
  				continue;
  
  			// Apply cached settings from .res file
  			if ( m_pPlayerCellData )
  			{
  				pCurCell->ApplySettings( m_pPlayerCellData );
  			}
  
  			const int nY = nPanelHeight/2 + m_nRedBlueSigns[i] * nPanelHeight/4 - pCurCell->GetTall()/2;
  			pCurCell->SetPos(
  				nCurX,
  				nY
  			);
  
  			nCurX += pCurCell->GetWide() + nCellBuffer;
  		}
  	}
  }
  
  void CReplayPerformanceEditorPanel::PerformLayout() 
  {
  	int w = ScreenWidth(), h = ScreenHeight();
  	SetBounds(0,0,w,h);
  
  	// Layout camera options panels
  	for ( int i = 0; i < NCAMS; ++i )
  	{
  		CCameraOptionsPanel *pCurOptionsPanel = m_pCameraOptionsPanels[ i ];
  		if ( !pCurOptionsPanel )
  			continue;
  
  		CExImageButton *pCurCameraButton = m_pCameraButtons[ i ];	
  		if ( !pCurCameraButton )
  			continue;
  
  		// Get camera button position
  		int aCameraButtonPos[2];
  		int aBottomPos[2];
  		pCurCameraButton->GetPos( aCameraButtonPos[ 0 ], aCameraButtonPos[ 1 ] );
  		m_pBottom->GetPos( aBottomPos[ 0 ], aBottomPos[ 1 ] );
  
  		// Layout the panel now - it should set its own size, which we need to know to position it properly
  		pCurOptionsPanel->InvalidateLayout( true, true );
  
  		// Position it
  		pCurOptionsPanel->SetPos(
  			aBottomPos[ 0 ] + aCameraButtonPos[ 0 ] + pCurCameraButton->GetWide() - pCurOptionsPanel->GetWide() - XRES( 3 ),
  			aBottomPos[ 1 ] + aCameraButtonPos[ 1 ] - pCurOptionsPanel->GetTall()
  		);
  	}
  
  	// Setup menu position relative to menu button
  	int aMenuButtonPos[2];
  	m_pMenuButton->GetPos( aMenuButtonPos[0], aMenuButtonPos[1] );
  	m_pMenu->SetPos( aMenuButtonPos[0], aMenuButtonPos[1] + m_pMenuButton->GetTall() );
  
  	// Set player cell panel to be the size of half the bottom panel
  	int aBottomSize[2];
  	m_pBottom->GetSize( aBottomSize[0], aBottomSize[1] );
  	m_pPlayerCellsPanel->SetBounds( 0, 0, aBottomSize[0] / 2, m_pPlayerCellsPanel->GetTall() );
  
  	CExLabel *pRedBlueLabels[2] = {
  		dynamic_cast< CExLabel * >( m_pPlayerCellsPanel->FindChildByName( "RedLabel" ) ),
  		dynamic_cast< CExLabel * >( m_pPlayerCellsPanel->FindChildByName( "BlueLabel" ) )
  	};
  	int nMargins[2] = { (int)XRES( 5 ), (int)YRES( 2 ) };
  	for ( int i = 0; i < 2; ++i )
  	{
  		pRedBlueLabels[i]->SizeToContents();
  
  		const int nY = m_pPlayerCellsPanel->GetTall()/2 + m_nRedBlueSigns[i] * m_pPlayerCellsPanel->GetTall()/4 - pRedBlueLabels[i]->GetTall()/2;
  		pRedBlueLabels[i]->SetPos( nMargins[0], nY );
  
  		m_nRedBlueLabelRightX = MAX( m_nRedBlueLabelRightX, nMargins[0] + pRedBlueLabels[i]->GetWide() + nMargins[0] );
  	}
  
  	// Position player cells
  	LayoutPlayerCells();
  
  	BaseClass::PerformLayout();
  }
  
  bool CReplayPerformanceEditorPanel::OnStateChangeRequested( const char *pEventStr )
  {
  	// If we're already recording, allow the change.
  	if ( g_pReplayPerformanceController->IsRecording() )
  		return true;
  
  	// If we aren't recording and there is no forthcoming data in the playback stream, allow the change.
  	if ( !g_pReplayPerformanceController->IsPlaybackDataLeft() )
  		return true;
  
  	// Otherwise, record the event string and show a dialog asking the user if they're sure they want to nuke.
  	V_strncpy( m_szSuspendedEvent, pEventStr, sizeof( m_szSuspendedEvent ) );
  	ShowConfirmDialog( "#Replay_Warning", "#Replay_NukePerformanceChanges", "#GameUI_Confirm", "#GameUI_CancelBold", OnConfirmDestroyChanges, this, this, REPLAY_SOUND_DIALOG_POPUP );
  
  	return false;
  }
  
  void CReplayPerformanceEditorPanel::SetButtonTip( wchar_t *pTipText, Panel *pContextPanel )
  {
  	// Set the text
  	m_pButtonTip->SetText( pTipText );
  	m_pButtonTip->InvalidateLayout( true, true );
  
  	// Center relative to context panel
  	int aPos[2];
  	ipanel()->GetAbsPos( pContextPanel->GetVPanel(), aPos[0], aPos[1] );
  	const int nX = clamp(
  		aPos[0] - m_pButtonTip->GetWide() / 2,
  		0,
  		ScreenWidth() - m_pButtonTip->GetWide() - (int) XRES( 40 )
  	);
  	const int nY = m_nBottomPanelStartY - m_pButtonTip->GetTall() - (int) YRES( 2 );
  	m_pButtonTip->SetPos( nX, nY );
  }
  
  void CReplayPerformanceEditorPanel::ShowButtonTip( bool bShow )
  {
  	m_pButtonTip->SetVisible( bShow );
  }
  
  void CReplayPerformanceEditorPanel::ShowSavingDialog()
  {
  	Assert( !m_pSavingDlg );
  	m_pSavingDlg = new CSavingDialog( ReplayUI_GetPerformanceEditor() );
  	ShowWaitingDialog( m_pSavingDlg, "#Replay_Saving", true, false, -1 );
  }
  
  void CReplayPerformanceEditorPanel::ShowPanel( bool bShow )
  {
  	if ( bShow == IsVisible() )
  		return;
  
  	if ( bShow )
  	{
  		// We are now performing.
  		m_pRecLightPanel->SetPerforming( true );
  
  		// Disable keyboard input on all panels added to the list
  		FOR_EACH_LL( m_lstDisableKeyboardInputPanels, it )
  		{
  			m_lstDisableKeyboardInputPanels[ it ]->SetKeyBoardInputEnabled( false );
  		}
  
  		DisplayPerformanceTip( "#Replay_PerfTip_ExitPerfMode", &replay_perftip_count_exit, MAX_TIP_DISPLAYS );
  
  		// Fire a message the game DLL can intercept (for achievements, etc).
  		IGameEvent *event = gameeventmanager->CreateEvent( "entered_performance_mode" );
  		if ( event )
  		{
  			gameeventmanager->FireEventClientSide( event );
  		}
  
  		// Play a sound
  		surface()->PlaySound( "replay\\enterperformancemode.wav" );
  	}
  	else
  	{
  		// Display a tip
  		DisplayPerformanceTip( "#Replay_PerfTip_EnterPerfMode", &replay_perftip_count_enter, MAX_TIP_DISPLAYS );
  
  		// Play a sound
  		surface()->PlaySound( "replay\\exitperformancemode.wav" );
  	}
  
  	// Show mouse cursor
  	SetMouseInputEnabled( bShow );
  	SetVisible( bShow );
  	MakePopup( bShow );
  
  	// Avoid waiting for next OnThink() to hide background images
  	m_pRecLightPanel->UpdatePauseState( bShow );
  	m_pRecLightPanel->UpdateBackgroundVisibility();
  
  	// Play or pause
  	if ( bShow )
  	{
  		PauseDemo();
  	}
  	else
  	{
  		PlayDemo();
  	}
  
  	// Keep controller informed about pause state so that it can throw away unimportant events during pause if it's recording.
  	g_pReplayPerformanceController->NotifyPauseState( bShow );
  }
  
  bool CReplayPerformanceEditorPanel::OnEndOfReplayReached()
  {
  	if ( m_bShownAtLeastOnce )
  	{
  		ShowPanel( true );
  		DisplayPerformanceTip( "#Replay_PerfTip_EndOfReplayReached" );
  
  		// Don't end demo playback yet.
  		return true;
  	}
  
  	// Let the demo player end demo playback
  	return false;
  }
  
  void CReplayPerformanceEditorPanel::AddSetViewEvent()
  {
  	if ( !g_pReplayManager->GetPlayingReplay() )
  		return;
  
  	if ( !g_pReplayPerformanceController )
  		return;
  
  	Vector pos;
  	QAngle angles;
  	float fov;
  	ReplayCamera()->GetCachedView( pos, angles, fov );
  
  	SetViewParams_t params;
  	params.m_flTime = GetPlaybackTime();
  	params.m_flFov = fov;
  	params.m_pOrigin = &pos;
  	params.m_pAngles = &angles;
  
  	params.m_flAccel = ReplayCamera()->m_flRoamingAccel;
  	params.m_flSpeed = ReplayCamera()->m_flRoamingSpeed;
  	params.m_flRotationFilter = ReplayCamera()->m_flRoamingRotFilterFactor;
  
  	g_pReplayPerformanceController->AddEvent_Camera_SetView( params );
  }
  
  // Input should be in [0,1]
  void CReplayPerformanceEditorPanel::AddTimeScaleEvent( float flTimeScale )
  {
  	if ( !g_pReplayManager->GetPlayingReplay() )
  		return;
  
  	if ( !g_pReplayPerformanceController )
  		return;
  
  	g_pReplayPerformanceController->AddEvent_TimeScale( GetPlaybackTime(), flTimeScale );
  }
  
  void CReplayPerformanceEditorPanel::UpdateCameraButtonImages( bool bForceUnselected/*=false*/ )
  {
  	CReplayPerformance *pPerformance = GetPerformance();
  	for ( int i = 0; i < NCAMS; ++i )
  	{
  		CFmtStr fmtFile(
  			gs_pBaseComponentNames[i],
  			gs_pCamNames[i],
  			( !bForceUnselected && ( !pPerformance || g_pReplayPerformanceController->IsRecording() ) && i == m_iCameraSelection ) ? "_selected" : ""
  		);
  
  		if ( m_pCameraButtons[ i ] )
  		{
  			m_pCameraButtons[ i ]->SetSubImage( fmtFile.Access() );
  		}
  	}
  }
  
  void CReplayPerformanceEditorPanel::EnsureRecording( bool bShouldSnip )
  {
  	// Not recording?
  	if ( !g_pReplayPerformanceController->IsRecording() )
  	{
  		// Start recording - snip if needed.
  		g_pReplayPerformanceController->StartRecording( GetReplay(), bShouldSnip );
  	}
  }
  
  void CReplayPerformanceEditorPanel::ToggleMenu()
  {
  	if ( !m_pMenu )
  		return;
  
  	// Show/hide
  	const bool bShow = !m_pMenu->IsVisible();
  	m_pMenu->SetVisible( bShow );
  }
  
  void CReplayPerformanceEditorPanel::SaveAs( const wchar_t *pTitle )
  {
  	if ( !g_pReplayPerformanceController->SaveAsAsync( pTitle ) )
  	{
  		DisplaySavedTip( false );
  	}
  
  	ShowSavingDialog();
  }
  
  /*static*/ void CReplayPerformanceEditorPanel::OnConfirmSaveAs( bool bShouldSave, wchar_t *pTitle, void *pContext )
  {
  	// NOTE: Assumes that overwriting has already been confirmed by the user.
  
  	if ( !bShouldSave )
  		return;
  
  	CReplayPerformanceEditorPanel *pThis = (CReplayPerformanceEditorPanel *)pContext;
  	pThis->SaveAs( pTitle );
  
  	surface()->PlaySound( "replay\\saved_take.wav" );
  }
  
  void CReplayPerformanceEditorPanel::ShowRewindConfirmMessage()
  {
  	ShowMessageBox( "#Replay_RewindWarningTitle", "#Replay_RewindWarningMsg", "#GameUI_OK", OnConfirmRewind, NULL, (void *)this );
  	surface()->PlaySound( "replay\\replaydialog_warn.wav" );
  }
  
  /*static*/ void	CReplayPerformanceEditorPanel::OnConfirmRewind( bool bConfirmed, void *pContext )
  {
  	if ( bConfirmed )
  	{
  		if ( pContext )
  		{
  			CReplayPerformanceEditorPanel *pEditor = (CReplayPerformanceEditorPanel *)pContext;
  			pEditor->OnCommand( "goto_back" );
  		}
  	}
  }
  
  void CReplayPerformanceEditorPanel::OnMenuCommand_Save( bool bExitEditorWhenDone/*=false*/ )
  {
  	// If this is the first time we're saving this performance, do a save-as.
  	if ( !g_pReplayPerformanceController->HasSavedPerformance() )
  	{
  		OnMenuCommand_SaveAs( bExitEditorWhenDone );
  		return;
  	}
  
  	// Regular save
  	if ( !g_pReplayPerformanceController->SaveAsync() )
  	{
  		DisplaySavedTip( false );
  	}
  
  	// Show saving dialog
  	ShowSavingDialog();
  
  	// Exit editor?
  	if ( bExitEditorWhenDone )
  	{
  		OnMenuCommand_Exit();
  	}
  }
  
  void CReplayPerformanceEditorPanel::OnMenuCommand_SaveAs( bool bExitEditorWhenDone/*=false*/ )
  {
  	ReplayUI_ShowPerformanceSaveDlg( OnConfirmSaveAs, this, GetReplay(), bExitEditorWhenDone );
  }
  
  void CReplayPerformanceEditorPanel::DisplaySavedTip( bool bSucceess )
  {
  	DisplayPerformanceTip( bSucceess ? "#Replay_PerfTip_Saved" : "#Replay_PerfTip_SaveFailed" );
  }
  
  void CReplayPerformanceEditorPanel::OnSaveComplete()
  {
  	DisplaySavedTip( g_pReplayPerformanceController->GetLastSaveStatus() );	
  
  	m_pSavingDlg = NULL;
  }
  
  void CReplayPerformanceEditorPanel::HandleUiToggle()
  {
  	if ( !TFModalStack()->IsEmpty() )
  		return;
  
  	PauseDemo();
  	Exit_ShowDialogs();
  }
  
  void CReplayPerformanceEditorPanel::Exit()
  {
  	engine->ClientCmd_Unrestricted( "disconnect" );
  }
  
  void CReplayPerformanceEditorPanel::Exit_ShowDialogs()
  {
  	if ( g_pReplayPerformanceController->IsDirty() )
  	{
  		ShowConfirmDialog( "#Replay_DiscardTitle", "#Replay_DiscardChanges", "#Replay_Discard", "#Replay_Cancel", OnConfirmDiscard, NULL, this, REPLAY_SOUND_DIALOG_POPUP );
  	}
  	else
  	{
  		ShowConfirmDialog( "#Replay_ExitEditorTitle", "#Replay_BackToReplays", "#GameUI_Confirm", "#Replay_Cancel", OnConfirmExit, NULL, this, REPLAY_SOUND_DIALOG_POPUP );
  	}
  }
  
  void CReplayPerformanceEditorPanel::OnMenuCommand_Exit()
  {
  	Exit_ShowDialogs();
  }
  
  void CReplayPerformanceEditorPanel::OnCommand( const char *command )
  {
  	float flCurTime = GetPlaybackTime();
  
  	g_bIsReplayRewinding = false;
  
  	if ( !V_stricmp( command, "toggle_menu" ) )
  	{
  		ToggleMenu();
  	}
  	else if ( !V_strnicmp( command, "menu_", 5 ) )
  	{
  		const char *pMenuCommand = command + 5;
  
  		if ( !V_stricmp( pMenuCommand, "save" ) )
  		{
  			OnMenuCommand_Save();
  		}
  		else if ( !V_stricmp( pMenuCommand, "saveas" ) )
  		{
  			OnMenuCommand_SaveAs();
  		}
  		else if ( !V_stricmp( pMenuCommand, "exit" ) )
  		{
  			OnMenuCommand_Exit();
  		}
  	}
  	else if ( !V_stricmp( command, "close" ) )
  	{
  		ShowPanel( false );
  		MarkForDeletion();
  		return;
  	}
  	else if ( !V_stricmp( command, "play" ) )
  	{
  		ShowPanel( false );
  		return;
  	}
  	else if ( !V_stricmp( command, "pause" ) )
  	{
  		ShowPanel( true );
  		return;
  	}
  	else if ( !V_strnicmp( command, "timescale_", 10 ) )
  	{
  		const char *pTimeScaleCmd = command + 10;
  		if ( !V_stricmp( pTimeScaleCmd, "showpanel" ) )
  		{
  			// If we're playing back, pop up a dialog asking if the user is sure they want to nuke the
  			// rest of whatever is playing back.
  			if ( !OnStateChangeRequested( command ) )
  				return;
  
  			EnsureRecording();
  		}
  	}
  	else if ( !V_strnicmp( command, "settick_", 8 ) )
  	{
  		const char *pSetType = command + 8;
  		const int nCurTick = engine->GetDemoPlaybackTick();
  
  		if ( !V_stricmp( pSetType, "in" ) )
  		{
  			SetOrRemoveInTick( nCurTick, true );
  		}
  		else if ( !V_stricmp( pSetType, "out" ) )
  		{
  			SetOrRemoveOutTick( nCurTick, true );
  		}
  
  		// Save the replay
  		CReplay *pReplay = GetReplay();
  		if ( pReplay )
  		{
  			g_pReplayManager->FlagReplayForFlush( pReplay, true );
  		}
  
  		return;
  	}
  	else if ( !V_strnicmp( command, "goto_", 5 ) )
  	{
  		const char *pGotoType = command + 5;
  		CReplay *pReplay = GetReplay();
  		if ( pReplay )
  		{
  			const CReplayPerformance *pScratchPerformance = g_pReplayPerformanceController->GetPerformance();
  			const CReplayPerformance *pSavedPerformance = g_pReplayPerformanceController->GetSavedPerformance();
  			const CReplayPerformance *pPerformance = pScratchPerformance ? pScratchPerformance : pSavedPerformance;
  
  			const int nCurTick = engine->GetDemoPlaybackTick();
  
  			// If in or out ticks are set in the performance, use those for the 'full' rewind/fast-forward
  			const int nStartTick = MAX( 0, ( pPerformance && pPerformance->HasInTick() ) ? pPerformance->m_nTickIn : pReplay->m_nSpawnTick );
  			const int nEndTick = MAX(	// The MAX() here will keep us from going back in time if we're already past the "end" tick
  				nCurTick,
  				( ( pPerformance && pPerformance->HasOutTick() ) ?
  					pPerformance->m_nTickOut :
  					( nStartTick + TIME_TO_TICKS( pReplay->m_flLength ) ) )
  				- TIME_TO_TICKS( 0.1f )
  			);
  
  			int nGotoTick = 0;
  			bool bGoingBack = false;
  
  			if ( !V_stricmp( pGotoType, "start" ) )
  			{
  				bGoingBack = true;
  				nGotoTick = nStartTick;
  			}
  			else if ( !V_stricmp( pGotoType, "back" ) )
  			{
  				// If this is the first time rewinding, display a message
  				if ( !replay_replayeditor_rewindmsgcounter.GetBool() )
  				{
  					replay_replayeditor_rewindmsgcounter.SetValue( 1 );
  					ShowRewindConfirmMessage();
  					return;
  				}
  
  				bGoingBack = true;
  				nGotoTick = nCurTick - TIME_TO_TICKS( 10.0f );
  			}
  			else if ( !V_stricmp( pGotoType, "end" ) )
  			{
  				nGotoTick = nEndTick;	// Don't go back in time
  			}
  
  			// Clamp it
  			nGotoTick = clamp( nGotoTick, nStartTick, nEndTick );
  
  			// If going back...
  			if ( bGoingBack )
  			{
  				// ...and notify the recorder that we're skipping, which we only need to do if we're going backwards
  				g_pReplayPerformanceController->NotifyRewinding();
  				g_bIsReplayRewinding = true;
  			}
  
  			// Go to the given tick and pause
  			CFmtStr fmtCmd( "demo_gototick %i\ndemo_pause\n", nGotoTick );
  			engine->ClientCmd_Unrestricted( fmtCmd.Access() );
  		}
  		return;
  	}
  	else if ( !V_strnicmp( command, "setcamera_", 10 ) )
  	{
  		const char *pCamType = command + 10;
  		int nEntIndex = ReplayCamera()->GetPrimaryTargetIndex();
  
  		// If we're playing back, pop up a dialog asking if the user is sure they want to nuke the
  		// rest of whatever is playing back.
  		if ( !OnStateChangeRequested( command ) )
  			return;
  
  		EnsureRecording();
  
  		if ( !V_stricmp( pCamType, "first" ) )
  		{
  			ReplayCamera()->SetMode( OBS_MODE_IN_EYE );
  			UpdateCameraSelectionPosition( CAM_FIRST );
  			m_bCurrentTargetNeedsVisibilityUpdate = true;
  			g_pReplayPerformanceController->AddEvent_Camera_Change_FirstPerson( flCurTime, nEntIndex );
  		}
  		else if ( !V_stricmp( pCamType, "third" ) )
  		{
  			ReplayCamera()->SetMode( OBS_MODE_CHASE );
  			UpdateCameraSelectionPosition( CAM_THIRD );
  			m_bCurrentTargetNeedsVisibilityUpdate = true;
  			g_pReplayPerformanceController->AddEvent_Camera_Change_ThirdPerson( flCurTime, nEntIndex );
  			AddSetViewEvent();
  		}
  		else if ( !V_stricmp( pCamType, "free" ) )
  		{
  			ReplayCamera()->SetMode( OBS_MODE_ROAMING );
  			UpdateCameraSelectionPosition( CAM_FREE );
  			m_bCurrentTargetNeedsVisibilityUpdate = true;
  			g_pReplayPerformanceController->AddEvent_Camera_Change_Free( flCurTime );
  			AddSetViewEvent();
  			DisplayPerformanceTip( "#Replay_PerfTip_EnterFreeCam", &replay_perftip_count_freecam_enter, MAX_TIP_DISPLAYS );
  		}
  
  		return;
  	}
  	else
  	{
  		engine->ClientCmd( const_cast<char *>( command ) );
  		return;
  	}
  
  	BaseClass::OnCommand( command );
  }
  
  void CReplayPerformanceEditorPanel::OnConfirmDestroyChanges( bool bConfirmed, void *pContext )
  {
  	AssertMsg( pContext, "Should have a context!  Fix me!" );
  	if ( pContext && bConfirmed )
  	{
  		CReplayPerformanceEditorPanel *pEditorPanel = (CReplayPerformanceEditorPanel *)pContext;
  		if ( bConfirmed )
  		{
  			CReplay *pReplay = pEditorPanel->GetReplay();
  			g_pReplayPerformanceController->StartRecording( pReplay, true );
  
  			// Reissue the command.
  			pEditorPanel->OnCommand( pEditorPanel->m_szSuspendedEvent );
  
  			// Play a sound
  			surface()->PlaySound( "replay\\snip.wav" );
  		}
  
  		// Clear suspended event
  		pEditorPanel->m_szSuspendedEvent[ 0 ] = '\0';
  
  		// Make sure mouse is free
  		pEditorPanel->SetMouseInputEnabled( true );
  
  		DisplayPerformanceTip( "#Replay_PerfTip_Snip" );
  	}
  }
  
  /*static*/ void CReplayPerformanceEditorPanel::OnConfirmDiscard( bool bConfirmed, void *pContext )
  {
  	CReplayPerformanceEditorPanel *pEditor = (CReplayPerformanceEditorPanel *)pContext;
  	if ( bConfirmed )
  	{
  		pEditor->Exit();
  	}
  	else
  	{
  		if ( !pEditor->IsVisible() )
  		{
  			PlayDemo();
  		}
  	}
  }
  
  /*static*/ void CReplayPerformanceEditorPanel::OnConfirmExit( bool bConfirmed, void *pContext )
  {
  	CReplayPerformanceEditorPanel *pEditor = (CReplayPerformanceEditorPanel *)pContext;
  	if ( bConfirmed )
  	{
  		pEditor->Exit();
  	}
  	else
  	{
  		if ( !pEditor->IsVisible() )
  		{
  			PlayDemo();
  		}
  	}
  }
  
  void CReplayPerformanceEditorPanel::SetOrRemoveInTick( int nTick, bool bRemoveIfSet )
  {
  	SetOrRemoveTick( nTick, true, bRemoveIfSet );
  }
  
  void CReplayPerformanceEditorPanel::SetOrRemoveOutTick( int nTick, bool bRemoveIfSet )
  {
  	SetOrRemoveTick( nTick, false, bRemoveIfSet );
  }
  
  void CReplayPerformanceEditorPanel::SetOrRemoveTick( int nTick, bool bUseInTick, bool bRemoveIfSet )
  {
  	CReplayPerformance *pPerformance = GetPerformance();
  	AssertMsg( pPerformance, "Performance should always be valid by this point." );
  
  	ControlButtons_t iButton;
  	int *pResultTick;
  	const char *pSetTickKey;
  	const char *pUnsetTickKey;
  	if ( bUseInTick )
  	{
  		pResultTick = &pPerformance->m_nTickIn;
  		iButton = CTRLBUTTON_IN;
  		pSetTickKey = "#Replay_PerfTip_InPointSet";
  		pUnsetTickKey = "#Replay_PerfTip_InPointRemoved";
  	}
  	else
  	{
  		pResultTick = &pPerformance->m_nTickOut;
  		iButton = CTRLBUTTON_OUT;
  		pSetTickKey = "#Replay_PerfTip_OutPointSet";
  		pUnsetTickKey = "#Replay_PerfTip_OutPointRemoved";
  	}
  
  	// Tick explicitly being removed?  Caller passing in -1?
  	const bool bRemoving = nTick < 0;
  
  	// If tick already exists and we want to remove, remove it
  	bool bSetting;
  	if ( ( *pResultTick >= 0 && bRemoveIfSet ) || bRemoving )
  	{
  		*pResultTick = -1;
  		bSetting = false;
  	}
  	else
  	{
  		*pResultTick = nTick;
  		bSetting = true;
  	}
  
  	// Display the appropriate tip
  	DisplayPerformanceTip( bSetting ? pSetTickKey : pUnsetTickKey );
  
  	// Select/unselect button
  	CExImageButton *pButton = m_pCtrlButtons[ iButton ];
  	pButton->SetSelected( bSetting );
  	pButton->InvalidateLayout( true, true );	// Without this, buttons don't update immediately
  
  	// Mark the performance as dirty
  	g_pReplayPerformanceController->NotifyDirty();
  }
  
  CReplay *CReplayPerformanceEditorPanel::GetReplay()
  {
  	return g_pReplayManager->GetReplay( m_hReplay );
  }
  
  void CReplayPerformanceEditorPanel::OnRewindComplete()
  {
  	// Get rid of any "selected" icon - this will happen as soon as we actually start playing back
  	// events, but if we aren't playing back events yet we need to explicitly tell the icons not
  	// to display their "selected" versions.
  	UpdateCameraButtonImages( true );
  }
  
  //-----------------------------------------------------------------------------
  
  static DHANDLE<CReplayPerformanceEditorPanel> g_ReplayPerformanceEditorPanel;
  
  //-----------------------------------------------------------------------------
  
  CReplayPerformanceEditorPanel *ReplayUI_InitPerformanceEditor( ReplayHandle_t hReplay )
  {
  	if ( !g_ReplayPerformanceEditorPanel.Get() )
  	{
  		g_ReplayPerformanceEditorPanel = SETUP_PANEL( new CReplayPerformanceEditorPanel( NULL, hReplay ) );
  		g_ReplayPerformanceEditorPanel->InvalidateLayout( false, true );
  	}
  
  	// Notify recorder of editor
  	g_pReplayPerformanceController->SetEditor( g_ReplayPerformanceEditorPanel.Get() );
  
  	return g_ReplayPerformanceEditorPanel;
  }
  
  void ReplayUI_ClosePerformanceEditor()
  {
  	if ( g_ReplayPerformanceEditorPanel )
  	{
  		g_ReplayPerformanceEditorPanel->MarkForDeletion();
  		g_ReplayPerformanceEditorPanel = NULL;
  	}
  }
  
  CReplayPerformanceEditorPanel *ReplayUI_GetPerformanceEditor()
  {
  	return g_ReplayPerformanceEditorPanel;
  }
  
  #if _DEBUG
  CON_COMMAND_F( replay_showperfeditor, "Show performance editor", FCVAR_CLIENTDLL )
  {
  	ReplayUI_ClosePerformanceEditor();
  	ReplayUI_InitPerformanceEditor( REPLAY_HANDLE_INVALID );
  }
  
  CON_COMMAND_F( replay_tiptest, "", FCVAR_CLIENTDLL )
  {
  	DisplayPerformanceTip( "#Replay_PerfTip_EnterFreeCam" );
  }
  #endif
  
  #endif
  