ddnet/src/game/client/components/menus_demo.cpp
2023-11-26 17:29:40 +01:00

1627 lines
56 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#include <base/hash.h>
#include <base/math.h>
#include <base/system.h>
#include <engine/demo.h>
#include <engine/graphics.h>
#include <engine/keys.h>
#include <engine/shared/localization.h>
#include <engine/storage.h>
#include <engine/textrender.h>
#include <game/client/components/console.h>
#include <game/client/gameclient.h>
#include <game/client/render.h>
#include <game/client/ui.h>
#include <game/client/ui_listbox.h>
#include <game/generated/client_data.h>
#include <game/localization.h>
#include "maplayers.h"
#include "menus.h"
#include <chrono>
using namespace FontIcons;
using namespace std::chrono_literals;
int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, bool Enabled)
{
pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, (Checked ? 0.10f : 0.5f) * UI()->ButtonColorMul(pButtonContainer)), Corners, 5.0f);
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
TextRender()->TextColor(TextRender()->DefaultTextColor());
CUIRect Temp;
pRect->HMargin(2.0f, &Temp);
UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC);
if(!Enabled)
{
TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
TextRender()->TextOutlineColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f));
UI()->DoLabel(&Temp, FONT_ICON_SLASH, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC);
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
TextRender()->SetRenderFlags(0);
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
return UI()->DoButtonLogic(pButtonContainer, Checked, pRect);
}
bool CMenus::DemoFilterChat(const void *pData, int Size, void *pUser)
{
bool DoFilterChat = *(bool *)pUser;
if(!DoFilterChat)
{
return false;
}
CUnpacker Unpacker;
Unpacker.Reset(pData, Size);
int Msg = Unpacker.GetInt();
int Sys = Msg & 1;
Msg >>= 1;
return !Unpacker.Error() && !Sys && Msg == NETMSGTYPE_SV_CHAT;
}
void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek)
{
if((PositionToSeek >= 0.0f && PositionToSeek <= 1.0f) || TimeToSeek != 0.0f)
{
m_pClient->m_Chat.Reset();
m_pClient->m_InfoMessages.OnReset();
m_pClient->m_Particles.OnReset();
m_pClient->m_Sounds.OnReset();
m_pClient->m_Scoreboard.OnReset();
m_pClient->m_Statboard.OnReset();
m_pClient->m_SuppressEvents = true;
if(TimeToSeek != 0.0f)
DemoPlayer()->SeekTime(TimeToSeek);
else
DemoPlayer()->SeekPercent(PositionToSeek);
m_pClient->m_SuppressEvents = false;
m_pClient->m_MapLayersBackground.EnvelopeUpdate();
m_pClient->m_MapLayersForeground.EnvelopeUpdate();
if(!DemoPlayer()->BaseInfo()->m_Paused && PositionToSeek == 1.0f)
DemoPlayer()->Pause();
}
}
void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset)
{
m_pClient->m_SuppressEvents = true;
DemoPlayer()->SeekTick(TickOffset);
m_pClient->m_SuppressEvents = false;
DemoPlayer()->Pause();
m_pClient->m_MapLayersBackground.EnvelopeUpdate();
m_pClient->m_MapLayersForeground.EnvelopeUpdate();
}
void CMenus::RenderDemoPlayer(CUIRect MainView)
{
const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
const int CurrentTick = pInfo->m_CurrentTick - pInfo->m_FirstTick;
const int TotalTicks = pInfo->m_LastTick - pInfo->m_FirstTick;
// When rendering a demo and starting paused, render the pause indicator permanently.
#if defined(CONF_VIDEORECORDER)
const bool VideoRendering = IVideo::Current() != nullptr;
bool InitialVideoPause = VideoRendering && m_LastPauseChange < 0.0f && pInfo->m_Paused;
#else
const bool VideoRendering = false;
bool InitialVideoPause = false;
#endif
const auto &&UpdateLastPauseChange = [&]() {
// Immediately hide the pause indicator when unpausing the initial pause when rendering a demo.
m_LastPauseChange = InitialVideoPause ? 0.0f : Client()->GlobalTime();
InitialVideoPause = false;
};
const auto &&UpdateLastSpeedChange = [&]() {
m_LastSpeedChange = Client()->GlobalTime();
};
// threshold value, accounts for slight inaccuracy when setting demo position
constexpr int Threshold = 10;
const auto &&FindPreviousMarkerPosition = [&]() {
for(int i = pInfo->m_NumTimelineMarkers - 1; i >= 0; i--)
{
if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) < CurrentTick && absolute(((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
{
return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
}
}
return 0.0f;
};
const auto &&FindNextMarkerPosition = [&]() {
for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
{
if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) > CurrentTick && absolute(((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
{
return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
}
}
return 1.0f;
};
static int s_SkipDurationIndex = 1;
static const int s_aSkipDurationsSeconds[] = {1, 5, 10, 30, 60, 5 * 60, 10 * 60};
const int DemoLengthSeconds = TotalTicks / Client()->GameTickSpeed();
int NumDurationLabels = 0;
for(size_t i = 0; i < std::size(s_aSkipDurationsSeconds); ++i)
{
if(s_aSkipDurationsSeconds[i] >= DemoLengthSeconds)
break;
NumDurationLabels = i + 1;
}
if(NumDurationLabels > 0 && s_SkipDurationIndex >= NumDurationLabels)
s_SkipDurationIndex = maximum(0, NumDurationLabels - 1);
// handle keyboard shortcuts independent of active menu
float PositionToSeek = -1.0f;
float TimeToSeek = 0.0f;
if(m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts && !UI()->IsPopupOpen())
{
// increase/decrease speed
if(!Input()->ModifierIsPressed() && !Input()->ShiftIsPressed() && !Input()->AltIsPressed())
{
if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP) || Input()->KeyPress(KEY_UP))
{
DemoPlayer()->AdjustSpeedIndex(+1);
UpdateLastSpeedChange();
}
else if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN) || Input()->KeyPress(KEY_DOWN))
{
DemoPlayer()->AdjustSpeedIndex(-1);
UpdateLastSpeedChange();
}
}
// pause/unpause
if(Input()->KeyPress(KEY_SPACE) || Input()->KeyPress(KEY_RETURN) || Input()->KeyPress(KEY_KP_ENTER) || Input()->KeyPress(KEY_K))
{
if(pInfo->m_Paused)
{
DemoPlayer()->Unpause();
}
else
{
DemoPlayer()->Pause();
}
UpdateLastPauseChange();
}
// seek backward/forward configured time
if(Input()->KeyPress(KEY_LEFT) || Input()->KeyPress(KEY_J))
{
if(Input()->ModifierIsPressed())
PositionToSeek = FindPreviousMarkerPosition();
else if(Input()->ShiftIsPressed())
s_SkipDurationIndex = maximum(s_SkipDurationIndex - 1, 0);
else
TimeToSeek = -s_aSkipDurationsSeconds[s_SkipDurationIndex];
}
else if(Input()->KeyPress(KEY_RIGHT) || Input()->KeyPress(KEY_L))
{
if(Input()->ModifierIsPressed())
PositionToSeek = FindNextMarkerPosition();
else if(Input()->ShiftIsPressed())
s_SkipDurationIndex = minimum(s_SkipDurationIndex + 1, NumDurationLabels - 1);
else
TimeToSeek = s_aSkipDurationsSeconds[s_SkipDurationIndex];
}
// seek to 0-90%
const int aSeekPercentKeys[] = {KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9};
for(unsigned i = 0; i < std::size(aSeekPercentKeys); i++)
{
if(Input()->KeyPress(aSeekPercentKeys[i]))
{
PositionToSeek = i * 0.1f;
break;
}
}
// seek to the beginning/end
if(Input()->KeyPress(KEY_HOME))
{
PositionToSeek = 0.0f;
}
else if(Input()->KeyPress(KEY_END))
{
PositionToSeek = 1.0f;
}
// Advance single frame forward/backward with period/comma key
if(Input()->KeyPress(KEY_PERIOD))
{
DemoSeekTick(IDemoPlayer::TICK_NEXT);
}
else if(Input()->KeyPress(KEY_COMMA))
{
DemoSeekTick(IDemoPlayer::TICK_PREVIOUS);
}
}
const float SeekBarHeight = 15.0f;
const float ButtonbarHeight = 20.0f;
const float NameBarHeight = 20.0f;
const float Margins = 5.0f;
const float TotalHeight = SeekBarHeight + ButtonbarHeight + NameBarHeight + Margins * 3;
if(!m_MenuActive)
{
// Render pause indicator
if(g_Config.m_ClDemoShowPause && (InitialVideoPause || (!VideoRendering && Client()->GlobalTime() - m_LastPauseChange < 0.5f)))
{
const float Time = InitialVideoPause ? 0.5f : ((Client()->GlobalTime() - m_LastPauseChange) / 0.5f);
const float Alpha = (Time < 0.5f ? Time : (1.0f - Time)) * 2.0f;
if(Alpha > 0.0f)
{
TextRender()->TextColor(TextRender()->DefaultTextColor().WithMultipliedAlpha(Alpha));
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(Alpha));
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
UI()->DoLabel(UI()->Screen(), pInfo->m_Paused ? FONT_ICON_PAUSE : FONT_ICON_PLAY, 36.0f + Time * 12.0f, TEXTALIGN_MC);
TextRender()->TextColor(TextRender()->DefaultTextColor());
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
TextRender()->SetRenderFlags(0);
}
}
// Render speed info
if(g_Config.m_ClDemoShowSpeed && Client()->GlobalTime() - m_LastSpeedChange < 1.0f)
{
CUIRect Screen = *UI()->Screen();
char aSpeedBuf[16];
str_format(aSpeedBuf, sizeof(aSpeedBuf), "×%.2f", pInfo->m_Speed);
TextRender()->Text(120.0f, Screen.y + Screen.h - 120.0f - TotalHeight, 60.0f, aSpeedBuf, -1.0f);
}
}
else
{
if(m_LastPauseChange > 0.0f)
m_LastPauseChange = 0.0f;
if(m_LastSpeedChange > 0.0f)
m_LastSpeedChange = 0.0f;
}
if(CurrentTick == TotalTicks)
{
DemoPlayer()->Pause();
PositionToSeek = 0.0f;
UpdateLastPauseChange();
}
if(!m_MenuActive)
{
HandleDemoSeeking(PositionToSeek, TimeToSeek);
return;
}
CUIRect DemoControls;
MainView.HSplitBottom(TotalHeight, nullptr, &DemoControls);
DemoControls.VSplitLeft(50.0f, nullptr, &DemoControls);
DemoControls.VSplitLeft(600.0f, &DemoControls, nullptr);
const CUIRect DemoControlsOriginal = DemoControls;
DemoControls.x += m_DemoControlsPositionOffset.x;
DemoControls.y += m_DemoControlsPositionOffset.y;
int Corners = IGraphics::CORNER_NONE;
if(DemoControls.x > 0.0f && DemoControls.y > 0.0f)
Corners |= IGraphics::CORNER_TL;
if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y > 0.0f)
Corners |= IGraphics::CORNER_TR;
if(DemoControls.x > 0.0f && DemoControls.y < MainView.h - DemoControls.h)
Corners |= IGraphics::CORNER_BL;
if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y < MainView.h - DemoControls.h)
Corners |= IGraphics::CORNER_BR;
DemoControls.Draw(ms_ColorTabbarActive, Corners, 10.0f);
const CUIRect DemoControlsDragRect = DemoControls;
CUIRect SeekBar, ButtonBar, NameBar, SpeedBar;
DemoControls.Margin(5.0f, &DemoControls);
DemoControls.HSplitTop(SeekBarHeight, &SeekBar, &ButtonBar);
ButtonBar.HSplitTop(Margins, nullptr, &ButtonBar);
ButtonBar.HSplitBottom(NameBarHeight, &ButtonBar, &NameBar);
NameBar.HSplitTop(4.0f, nullptr, &NameBar);
// handle draggable demo controls
{
enum EDragOperation
{
OP_NONE,
OP_DRAGGING,
OP_CLICKED
};
static EDragOperation s_Operation = OP_NONE;
static vec2 s_InitialMouse = vec2(0.0f, 0.0f);
bool Clicked;
bool Abrupted;
if(int Result = UI()->DoDraggableButtonLogic(&s_Operation, 8, &DemoControlsDragRect, &Clicked, &Abrupted))
{
if(s_Operation == OP_NONE && Result == 1)
{
s_InitialMouse = UI()->MousePos();
s_Operation = OP_CLICKED;
}
if(Clicked || Abrupted)
s_Operation = OP_NONE;
if(s_Operation == OP_CLICKED && length(UI()->MousePos() - s_InitialMouse) > 5.0f)
{
s_Operation = OP_DRAGGING;
s_InitialMouse -= m_DemoControlsPositionOffset;
}
if(s_Operation == OP_DRAGGING)
{
m_DemoControlsPositionOffset = UI()->MousePos() - s_InitialMouse;
m_DemoControlsPositionOffset.x = clamp(m_DemoControlsPositionOffset.x, -DemoControlsOriginal.x, MainView.w - DemoControlsDragRect.w - DemoControlsOriginal.x);
m_DemoControlsPositionOffset.y = clamp(m_DemoControlsPositionOffset.y, -DemoControlsOriginal.y, MainView.h - DemoControlsDragRect.h - DemoControlsOriginal.y);
}
}
}
// do seekbar
{
const float Rounding = 5.0f;
static int s_SeekBarID = 0;
void *pId = &s_SeekBarID;
char aBuffer[128];
// draw seek bar
SeekBar.Draw(ColorRGBA(0, 0, 0, 0.5f), IGraphics::CORNER_ALL, Rounding);
// draw filled bar
float Amount = CurrentTick / (float)TotalTicks;
CUIRect FilledBar = SeekBar;
FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
FilledBar.Draw(ColorRGBA(1, 1, 1, 0.5f), IGraphics::CORNER_ALL, Rounding);
// draw highlighting
if(g_Config.m_ClDemoSliceBegin != -1 && g_Config.m_ClDemoSliceEnd != -1)
{
float RatioBegin = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
float RatioEnd = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
float Span = ((SeekBar.w - 2 * Rounding) * RatioEnd) - ((SeekBar.w - 2 * Rounding) * RatioBegin);
Graphics()->TextureClear();
Graphics()->QuadsBegin();
Graphics()->SetColor(1.0f, 0.0f, 0.0f, 0.25f);
IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * RatioBegin, SeekBar.y, Span, SeekBar.h);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
// draw markers
for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
{
float Ratio = (pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / (float)TotalTicks;
Graphics()->TextureClear();
Graphics()->QuadsBegin();
Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f);
IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
// draw slice markers
// begin
if(g_Config.m_ClDemoSliceBegin != -1)
{
float Ratio = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
Graphics()->TextureClear();
Graphics()->QuadsBegin();
Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f);
IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
// end
if(g_Config.m_ClDemoSliceEnd != -1)
{
float Ratio = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
Graphics()->TextureClear();
Graphics()->QuadsBegin();
Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f);
IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h);
Graphics()->QuadsDrawTL(&QuadItem, 1);
Graphics()->QuadsEnd();
}
// draw time
char aCurrentTime[32];
str_time((int64_t)CurrentTick / Client()->GameTickSpeed() * 100, TIME_HOURS, aCurrentTime, sizeof(aCurrentTime));
char aTotalTime[32];
str_time((int64_t)TotalTicks / Client()->GameTickSpeed() * 100, TIME_HOURS, aTotalTime, sizeof(aTotalTime));
str_format(aBuffer, sizeof(aBuffer), "%s / %s", aCurrentTime, aTotalTime);
UI()->DoLabel(&SeekBar, aBuffer, SeekBar.h * 0.70f, TEXTALIGN_MC);
// do the logic
const bool Inside = UI()->MouseInside(&SeekBar);
if(UI()->CheckActiveItem(pId))
{
if(!UI()->MouseButton(0))
UI()->SetActiveItem(nullptr);
else
{
static float s_PrevAmount = 0.0f;
float AmountSeek = clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f);
if(Input()->ShiftIsPressed())
{
AmountSeek = s_PrevAmount + (AmountSeek - s_PrevAmount) * 0.05f;
if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(s_PrevAmount - AmountSeek) >= 0.0001f)
{
PositionToSeek = AmountSeek;
}
}
else
{
if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(s_PrevAmount - AmountSeek) >= 0.001f)
{
s_PrevAmount = AmountSeek;
PositionToSeek = AmountSeek;
}
}
}
}
else if(UI()->HotItem() == pId)
{
if(UI()->MouseButton(0))
{
UI()->SetActiveItem(pId);
}
else
{
const int HoveredTick = (int)(clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f) * TotalTicks);
static char s_aHoveredTime[32];
str_time((int64_t)HoveredTick / Client()->GameTickSpeed() * 100, TIME_HOURS, s_aHoveredTime, sizeof(s_aHoveredTime));
GameClient()->m_Tooltips.DoToolTip(pId, &SeekBar, s_aHoveredTime);
}
}
if(Inside)
UI()->SetHotItem(pId);
}
bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false;
// do buttons
CUIRect Button;
// combined play and pause button
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_PlayPauseButton;
if(DoButton_FontIcon(&s_PlayPauseButton, pInfo->m_Paused ? FONT_ICON_PLAY : FONT_ICON_PAUSE, false, &Button, IGraphics::CORNER_ALL))
{
if(pInfo->m_Paused)
{
DemoPlayer()->Unpause();
}
else
{
DemoPlayer()->Pause();
}
UpdateLastPauseChange();
}
GameClient()->m_Tooltips.DoToolTip(&s_PlayPauseButton, &Button, pInfo->m_Paused ? Localize("Play the current demo") : Localize("Pause the current demo"));
// stop button
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_ResetButton;
if(DoButton_FontIcon(&s_ResetButton, FONT_ICON_STOP, false, &Button, IGraphics::CORNER_ALL))
{
DemoPlayer()->Pause();
PositionToSeek = 0.0f;
}
GameClient()->m_Tooltips.DoToolTip(&s_ResetButton, &Button, Localize("Stop the current demo"));
// skip time back
ButtonBar.VSplitLeft(Margins + 10.0f, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_TimeBackButton;
if(DoButton_FontIcon(&s_TimeBackButton, FONT_ICON_BACKWARD, 0, &Button, IGraphics::CORNER_ALL))
{
TimeToSeek = -s_aSkipDurationsSeconds[s_SkipDurationIndex];
}
GameClient()->m_Tooltips.DoToolTip(&s_TimeBackButton, &Button, Localize("Go back the specified duration"));
// skip time dropdown
if(NumDurationLabels >= 2)
{
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(4 * ButtonbarHeight, &Button, &ButtonBar);
static std::vector<std::string> s_vDurationNames;
static std::vector<const char *> s_vpDurationNames;
s_vDurationNames.resize(NumDurationLabels);
s_vpDurationNames.resize(NumDurationLabels);
for(int i = 0; i < NumDurationLabels; ++i)
{
char aBuf[256];
if(s_aSkipDurationsSeconds[i] >= 60)
str_format(aBuf, sizeof(aBuf), Localize("%d min.", "Demo player duration"), s_aSkipDurationsSeconds[i] / 60);
else
str_format(aBuf, sizeof(aBuf), Localize("%d sec.", "Demo player duration"), s_aSkipDurationsSeconds[i]);
s_vDurationNames[i] = aBuf;
s_vpDurationNames[i] = s_vDurationNames[i].c_str();
}
static CUI::SDropDownState s_SkipDurationDropDownState;
static CScrollRegion s_SkipDurationDropDownScrollRegion;
s_SkipDurationDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_SkipDurationDropDownScrollRegion;
s_SkipDurationIndex = UI()->DoDropDown(&Button, s_SkipDurationIndex, s_vpDurationNames.data(), NumDurationLabels, s_SkipDurationDropDownState);
GameClient()->m_Tooltips.DoToolTip(&s_SkipDurationDropDownState.m_ButtonContainer, &Button, Localize("Change the skip duration"));
}
// skip time forward
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_TimeForwardButton;
if(DoButton_FontIcon(&s_TimeForwardButton, FONT_ICON_FORWARD, 0, &Button, IGraphics::CORNER_ALL))
{
TimeToSeek = s_aSkipDurationsSeconds[s_SkipDurationIndex];
}
GameClient()->m_Tooltips.DoToolTip(&s_TimeForwardButton, &Button, Localize("Go forward the specified duration"));
// one tick back
ButtonBar.VSplitLeft(Margins + 10.0f, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneTickBackButton;
if(DoButton_FontIcon(&s_OneTickBackButton, FONT_ICON_BACKWARD_STEP, 0, &Button, IGraphics::CORNER_ALL))
{
DemoSeekTick(IDemoPlayer::TICK_PREVIOUS);
}
GameClient()->m_Tooltips.DoToolTip(&s_OneTickBackButton, &Button, Localize("Go back one tick"));
// one tick forward
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneTickForwardButton;
if(DoButton_FontIcon(&s_OneTickForwardButton, FONT_ICON_FORWARD_STEP, 0, &Button, IGraphics::CORNER_ALL))
{
DemoSeekTick(IDemoPlayer::TICK_NEXT);
}
GameClient()->m_Tooltips.DoToolTip(&s_OneTickForwardButton, &Button, Localize("Go forward one tick"));
// one marker back
ButtonBar.VSplitLeft(Margins + 10.0f, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneMarkerBackButton;
if(DoButton_FontIcon(&s_OneMarkerBackButton, FONT_ICON_BACKWARD_FAST, 0, &Button, IGraphics::CORNER_ALL))
{
PositionToSeek = FindPreviousMarkerPosition();
}
GameClient()->m_Tooltips.DoToolTip(&s_OneMarkerBackButton, &Button, Localize("Go back one marker"));
// one marker forward
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneMarkerForwardButton;
if(DoButton_FontIcon(&s_OneMarkerForwardButton, FONT_ICON_FORWARD_FAST, 0, &Button, IGraphics::CORNER_ALL))
{
PositionToSeek = FindNextMarkerPosition();
}
GameClient()->m_Tooltips.DoToolTip(&s_OneMarkerForwardButton, &Button, Localize("Go forward one marker"));
// slowdown
ButtonBar.VSplitLeft(Margins + 10.0f, 0, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_SlowDownButton;
if(DoButton_FontIcon(&s_SlowDownButton, FONT_ICON_CHEVRON_DOWN, 0, &Button, IGraphics::CORNER_ALL))
DecreaseDemoSpeed = true;
GameClient()->m_Tooltips.DoToolTip(&s_SlowDownButton, &Button, Localize("Slow down the demo"));
// fastforward
ButtonBar.VSplitLeft(Margins, 0, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_SpeedUpButton;
if(DoButton_FontIcon(&s_SpeedUpButton, FONT_ICON_CHEVRON_UP, 0, &Button, IGraphics::CORNER_ALL))
IncreaseDemoSpeed = true;
GameClient()->m_Tooltips.DoToolTip(&s_SpeedUpButton, &Button, Localize("Speed up the demo"));
// speed meter
ButtonBar.VSplitLeft(Margins * 12, &SpeedBar, &ButtonBar);
char aBuffer[64];
str_format(aBuffer, sizeof(aBuffer), "×%g", pInfo->m_Speed);
UI()->DoLabel(&SpeedBar, aBuffer, Button.h * 0.7f, TEXTALIGN_MC);
// slice begin button
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_SliceBeginButton;
const int SliceBeginButtonResult = DoButton_FontIcon(&s_SliceBeginButton, FONT_ICON_RIGHT_FROM_BRACKET, 0, &Button, IGraphics::CORNER_ALL);
if(SliceBeginButtonResult == 1)
{
Client()->DemoSliceBegin();
if(CurrentTick > (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick))
g_Config.m_ClDemoSliceEnd = -1;
}
else if(SliceBeginButtonResult == 2)
{
g_Config.m_ClDemoSliceBegin = -1;
}
GameClient()->m_Tooltips.DoToolTip(&s_SliceBeginButton, &Button, Localize("Mark the beginning of a cut (right click to reset)"));
// slice end button
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_SliceEndButton;
const int SliceEndButtonResult = DoButton_FontIcon(&s_SliceEndButton, FONT_ICON_RIGHT_TO_BRACKET, 0, &Button, IGraphics::CORNER_ALL);
if(SliceEndButtonResult == 1)
{
Client()->DemoSliceEnd();
if(CurrentTick < (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick))
g_Config.m_ClDemoSliceBegin = -1;
}
else if(SliceEndButtonResult == 2)
{
g_Config.m_ClDemoSliceEnd = -1;
}
GameClient()->m_Tooltips.DoToolTip(&s_SliceEndButton, &Button, Localize("Mark the end of a cut (right click to reset)"));
// slice save button
#if defined(CONF_VIDEORECORDER)
const bool SliceEnabled = IVideo::Current() == nullptr;
#else
const bool SliceEnabled = true;
#endif
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_SliceSaveButton;
if(DoButton_FontIcon(&s_SliceSaveButton, FONT_ICON_ARROW_UP_RIGHT_FROM_SQUARE, 0, &Button, IGraphics::CORNER_ALL, SliceEnabled) && SliceEnabled)
{
char aDemoName[IO_MAX_PATH_LENGTH];
DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName));
m_DemoSliceInput.Set(aDemoName);
UI()->SetActiveItem(&m_DemoSliceInput);
m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE;
}
GameClient()->m_Tooltips.DoToolTip(&s_SliceSaveButton, &Button, Localize("Export cut as a separate demo"));
// close button
ButtonBar.VSplitRight(ButtonbarHeight, &ButtonBar, &Button);
static CButtonContainer s_ExitButton;
if(DoButton_FontIcon(&s_ExitButton, FONT_ICON_XMARK, 0, &Button) || (Input()->KeyPress(KEY_C) && m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE))
{
Client()->Disconnect();
DemolistOnUpdate(false);
}
GameClient()->m_Tooltips.DoToolTip(&s_ExitButton, &Button, Localize("Close the demo player"));
// toggle keyboard shortcuts button
ButtonBar.VSplitRight(Margins, &ButtonBar, nullptr);
ButtonBar.VSplitRight(ButtonbarHeight, &ButtonBar, &Button);
static CButtonContainer s_KeyboardShortcutsButton;
if(DoButton_FontIcon(&s_KeyboardShortcutsButton, FONT_ICON_KEYBOARD, 0, &Button, IGraphics::CORNER_ALL, g_Config.m_ClDemoKeyboardShortcuts != 0))
{
g_Config.m_ClDemoKeyboardShortcuts ^= 1;
}
GameClient()->m_Tooltips.DoToolTip(&s_KeyboardShortcutsButton, &Button, Localize("Toggle keyboard shortcuts"));
// demo name
char aDemoName[IO_MAX_PATH_LENGTH];
DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName));
char aBuf[IO_MAX_PATH_LENGTH + 128];
str_format(aBuf, sizeof(aBuf), Localize("Demofile: %s"), aDemoName);
SLabelProperties Props;
Props.m_MaxWidth = NameBar.w;
Props.m_EllipsisAtEnd = true;
Props.m_EnableWidthCheck = false;
UI()->DoLabel(&NameBar, aBuf, Button.h * 0.5f, TEXTALIGN_ML, Props);
if(IncreaseDemoSpeed)
{
DemoPlayer()->AdjustSpeedIndex(+1);
UpdateLastSpeedChange();
}
else if(DecreaseDemoSpeed)
{
DemoPlayer()->AdjustSpeedIndex(-1);
UpdateLastSpeedChange();
}
HandleDemoSeeking(PositionToSeek, TimeToSeek);
// render popups
if(m_DemoPlayerState != DEMOPLAYER_NONE)
{
// prevent element under the active popup from being activated
UI()->SetHotItem(nullptr);
}
if(m_DemoPlayerState == DEMOPLAYER_SLICE_SAVE)
{
RenderDemoPlayerSliceSavePopup(MainView);
}
UI()->RenderPopupMenus();
}
void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView)
{
const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
CUIRect Box;
MainView.Margin(150.0f, &Box);
// background
Box.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f);
Box.Margin(24.0f, &Box);
// title
CUIRect Title;
Box.HSplitTop(24.0f, &Title, &Box);
Box.HSplitTop(20.0f, nullptr, &Box);
UI()->DoLabel(&Title, Localize("Export demo cut"), 24.0f, TEXTALIGN_MC);
// slice times
CUIRect SliceTimesBar, SliceInterval, SliceLength;
Box.HSplitTop(24.0f, &SliceTimesBar, &Box);
SliceTimesBar.VSplitMid(&SliceInterval, &SliceLength, 40.0f);
Box.HSplitTop(20.0f, nullptr, &Box);
const int64_t RealSliceBegin = g_Config.m_ClDemoSliceBegin == -1 ? 0 : (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick);
const int64_t RealSliceEnd = (g_Config.m_ClDemoSliceEnd == -1 ? pInfo->m_LastTick : g_Config.m_ClDemoSliceEnd) - pInfo->m_FirstTick;
char aSliceBegin[32];
str_time(RealSliceBegin / Client()->GameTickSpeed() * 100, TIME_HOURS, aSliceBegin, sizeof(aSliceBegin));
char aSliceEnd[32];
str_time(RealSliceEnd / Client()->GameTickSpeed() * 100, TIME_HOURS, aSliceEnd, sizeof(aSliceEnd));
char aSliceLength[32];
str_time((RealSliceEnd - RealSliceBegin) / Client()->GameTickSpeed() * 100, TIME_HOURS, aSliceLength, sizeof(aSliceLength));
char aBuf[256];
str_format(aBuf, sizeof(aBuf), "%s: %s %s", Localize("Cut interval"), aSliceBegin, aSliceEnd);
UI()->DoLabel(&SliceInterval, aBuf, 18.0f, TEXTALIGN_ML);
str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Cut length"), aSliceLength);
UI()->DoLabel(&SliceLength, aBuf, 18.0f, TEXTALIGN_ML);
// file name
CUIRect NameLabel, NameBox;
Box.HSplitTop(24.0f, &NameLabel, &Box);
Box.HSplitTop(20.0f, nullptr, &Box);
NameLabel.VSplitLeft(150.0f, &NameLabel, &NameBox);
NameBox.VSplitLeft(20.0f, nullptr, &NameBox);
UI()->DoLabel(&NameLabel, Localize("New name:"), 18.0f, TEXTALIGN_ML);
UI()->DoEditBox(&m_DemoSliceInput, &NameBox, 12.0f);
// remove chat checkbox
static int s_RemoveChat = 0;
CUIRect CheckBoxBar, RemoveChatCheckBox, RenderCutCheckBox;
Box.HSplitTop(24.0f, &CheckBoxBar, &Box);
Box.HSplitTop(20.0f, nullptr, &Box);
CheckBoxBar.VSplitMid(&RemoveChatCheckBox, &RenderCutCheckBox, 40.0f);
if(DoButton_CheckBox(&s_RemoveChat, Localize("Remove chat"), s_RemoveChat, &RemoveChatCheckBox))
{
s_RemoveChat ^= 1;
}
#if defined(CONF_VIDEORECORDER)
static int s_RenderCut = 0;
if(DoButton_CheckBox(&s_RenderCut, Localize("Render cut to video"), s_RenderCut, &RenderCutCheckBox))
{
s_RenderCut ^= 1;
}
#endif
// buttons
CUIRect ButtonBar, AbortButton, OkButton;
Box.HSplitBottom(24.0f, &Box, &ButtonBar);
ButtonBar.VSplitMid(&AbortButton, &OkButton, 40.0f);
static CButtonContainer s_ButtonAbort;
if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &AbortButton) || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)))
m_DemoPlayerState = DEMOPLAYER_NONE;
static CUI::SConfirmPopupContext s_ConfirmPopupContext;
static CButtonContainer s_ButtonOk;
if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &OkButton) || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)))
{
char aDemoName[IO_MAX_PATH_LENGTH];
char aNameWithoutExt[IO_MAX_PATH_LENGTH];
DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName));
fs_split_file_extension(m_DemoSliceInput.GetString(), aNameWithoutExt, sizeof(aNameWithoutExt));
m_DemoSliceInput.Set(aNameWithoutExt);
if(str_comp(aDemoName, m_DemoSliceInput.GetString()) == 0)
{
static CUI::SMessagePopupContext s_MessagePopupContext;
s_MessagePopupContext.ErrorColor();
str_copy(s_MessagePopupContext.m_aMessage, Localize("Please use a different filename"));
UI()->ShowPopupMessage(UI()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_MessagePopupContext);
}
else
{
char aPath[IO_MAX_PATH_LENGTH];
str_format(aPath, sizeof(aPath), "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
if(Storage()->FileExists(aPath, IStorage::TYPE_SAVE))
{
s_ConfirmPopupContext.Reset();
s_ConfirmPopupContext.YesNoButtons();
str_copy(s_ConfirmPopupContext.m_aMessage, Localize("File already exists, do you want to overwrite it?"));
UI()->ShowPopupConfirm(UI()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_ConfirmPopupContext);
}
else
s_ConfirmPopupContext.m_Result = CUI::SConfirmPopupContext::CONFIRMED;
}
}
if(s_ConfirmPopupContext.m_Result == CUI::SConfirmPopupContext::CONFIRMED)
{
char aPath[IO_MAX_PATH_LENGTH];
str_format(aPath, sizeof(aPath), "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
str_copy(m_aCurrentDemoSelectionName, m_DemoSliceInput.GetString());
if(str_endswith(m_aCurrentDemoSelectionName, ".demo"))
m_aCurrentDemoSelectionName[str_length(m_aCurrentDemoSelectionName) - str_length(".demo")] = '\0';
Client()->DemoSlice(aPath, CMenus::DemoFilterChat, &s_RemoveChat);
DemolistPopulate();
DemolistOnUpdate(false);
m_DemoPlayerState = DEMOPLAYER_NONE;
#if defined(CONF_VIDEORECORDER)
if(s_RenderCut)
{
m_Popup = POPUP_RENDER_DEMO;
m_StartPaused = false;
m_DemoRenderInput.Set(m_aCurrentDemoSelectionName);
UI()->SetActiveItem(&m_DemoRenderInput);
if(m_DemolistStorageType != IStorage::TYPE_ALL && m_DemolistStorageType != IStorage::TYPE_SAVE)
m_DemolistStorageType = IStorage::TYPE_ALL; // Select a storage type containing the sliced demo
}
#endif
}
if(s_ConfirmPopupContext.m_Result != CUI::SConfirmPopupContext::UNSET)
{
s_ConfirmPopupContext.Reset();
}
}
int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
{
CMenus *pSelf = (CMenus *)pUser;
if(str_comp(pInfo->m_pName, ".") == 0 ||
(str_comp(pInfo->m_pName, "..") == 0 && (pSelf->m_aCurrentDemoFolder[0] == '\0' || (!pSelf->m_DemolistMultipleStorages && str_comp(pSelf->m_aCurrentDemoFolder, "demos") == 0))) ||
(!IsDir && !str_endswith(pInfo->m_pName, ".demo")))
{
return 0;
}
CDemoItem Item;
str_copy(Item.m_aFilename, pInfo->m_pName);
if(IsDir)
{
str_format(Item.m_aName, sizeof(Item.m_aName), "%s/", pInfo->m_pName);
Item.m_Date = 0;
}
else
{
str_truncate(Item.m_aName, sizeof(Item.m_aName), pInfo->m_pName, str_length(pInfo->m_pName) - str_length(".demo"));
Item.m_Date = pInfo->m_TimeModified;
}
Item.m_InfosLoaded = false;
Item.m_Valid = false;
Item.m_IsDir = IsDir != 0;
Item.m_IsLink = false;
Item.m_StorageType = StorageType;
pSelf->m_vDemos.push_back(Item);
if(time_get_nanoseconds() - pSelf->m_DemoPopulateStartTime > 500ms)
{
pSelf->RenderLoading(Localize("Loading demo files"), "", 0, false);
}
return 0;
}
void CMenus::DemolistPopulate()
{
m_vDemos.clear();
int NumStoragesWithDemos = 0;
for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
{
if(Storage()->FolderExists("demos", StorageType))
{
NumStoragesWithDemos++;
}
}
m_DemolistMultipleStorages = NumStoragesWithDemos > 1;
if(m_aCurrentDemoFolder[0] == '\0')
{
{
CDemoItem Item;
str_copy(Item.m_aFilename, "demos");
str_copy(Item.m_aName, Localize("All combined"));
Item.m_InfosLoaded = false;
Item.m_Valid = false;
Item.m_Date = 0;
Item.m_IsDir = true;
Item.m_IsLink = true;
Item.m_StorageType = IStorage::TYPE_ALL;
m_vDemos.push_back(Item);
}
for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
{
if(Storage()->FolderExists("demos", StorageType))
{
CDemoItem Item;
str_copy(Item.m_aFilename, "demos");
Storage()->GetCompletePath(StorageType, "demos", Item.m_aName, sizeof(Item.m_aName));
str_append(Item.m_aName, "/", sizeof(Item.m_aName));
Item.m_InfosLoaded = false;
Item.m_Valid = false;
Item.m_Date = 0;
Item.m_IsDir = true;
Item.m_IsLink = true;
Item.m_StorageType = StorageType;
m_vDemos.push_back(Item);
}
}
}
else
{
m_DemoPopulateStartTime = time_get_nanoseconds();
Storage()->ListDirectoryInfo(m_DemolistStorageType, m_aCurrentDemoFolder, DemolistFetchCallback, this);
if(g_Config.m_BrDemoFetchInfo)
FetchAllHeaders();
std::stable_sort(m_vDemos.begin(), m_vDemos.end());
}
RefreshFilteredDemos();
}
void CMenus::RefreshFilteredDemos()
{
m_vpFilteredDemos.clear();
for(auto &Demo : m_vDemos)
{
if(str_find_nocase(Demo.m_aFilename, m_DemoSearchInput.GetString()))
{
m_vpFilteredDemos.push_back(&Demo);
}
}
}
void CMenus::DemolistOnUpdate(bool Reset)
{
if(Reset)
{
if(m_vpFilteredDemos.empty())
{
m_DemolistSelectedIndex = -1;
m_aCurrentDemoSelectionName[0] = '\0';
}
else
{
m_DemolistSelectedIndex = 0;
str_copy(m_aCurrentDemoSelectionName, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
}
}
else
{
RefreshFilteredDemos();
// search for selected index
m_DemolistSelectedIndex = -1;
int SelectedIndex = -1;
for(const auto &pItem : m_vpFilteredDemos)
{
SelectedIndex++;
if(str_comp(m_aCurrentDemoSelectionName, pItem->m_aName) == 0)
{
m_DemolistSelectedIndex = SelectedIndex;
break;
}
}
}
if(m_DemolistSelectedIndex >= 0)
m_DemolistSelectedReveal = true;
}
bool CMenus::FetchHeader(CDemoItem &Item)
{
if(!Item.m_InfosLoaded)
{
char aBuffer[IO_MAX_PATH_LENGTH];
str_format(aBuffer, sizeof(aBuffer), "%s/%s", m_aCurrentDemoFolder, Item.m_aFilename);
Item.m_Valid = DemoPlayer()->GetDemoInfo(Storage(), nullptr, aBuffer, Item.m_StorageType, &Item.m_Info, &Item.m_TimelineMarkers, &Item.m_MapInfo);
Item.m_InfosLoaded = true;
}
return Item.m_Valid;
}
void CMenus::FetchAllHeaders()
{
for(auto &Item : m_vDemos)
{
FetchHeader(Item);
}
std::stable_sort(m_vDemos.begin(), m_vDemos.end());
}
void CMenus::RenderDemoBrowser(CUIRect MainView)
{
CUIRect ListView, DetailsView, ButtonsView;
MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f);
MainView.Margin(10.0f, &MainView);
MainView.HSplitBottom(22.0f * 2.0f + 5.0f, &ListView, &ButtonsView);
ListView.VSplitRight(205.0f, &ListView, &DetailsView);
ListView.VSplitRight(5.0f, &ListView, nullptr);
bool WasListboxItemActivated;
RenderDemoBrowserList(ListView, WasListboxItemActivated);
RenderDemoBrowserDetails(DetailsView);
RenderDemoBrowserButtons(ButtonsView, WasListboxItemActivated);
}
void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivated)
{
if(!m_DemoBrowserListInitialized)
{
DemolistPopulate();
DemolistOnUpdate(true);
m_DemoBrowserListInitialized = true;
}
#if defined(CONF_VIDEORECORDER)
if(!m_DemoRenderInput.IsEmpty())
{
m_Popup = POPUP_RENDER_DONE;
}
#endif
struct SColumn
{
int m_Id;
int m_Sort;
const char *m_pCaption;
int m_Direction;
float m_Width;
CUIRect m_Rect;
};
enum
{
COL_ICON = 0,
COL_DEMONAME,
COL_LENGTH,
COL_DATE,
};
static CListBox s_ListBox;
static SColumn s_aCols[] = {
{-1, -1, "", -1, 2.0f, {0}},
{COL_ICON, -1, "", -1, ms_ListheaderHeight, {0}},
{-1, -1, "", -1, 2.0f, {0}},
{COL_DEMONAME, SORT_DEMONAME, Localizable("Demo"), 0, 0.0f, {0}},
{-1, -1, "", 1, 2.0f, {0}},
{COL_LENGTH, SORT_LENGTH, Localizable("Length"), 1, 75.0f, {0}},
{-1, -1, "", 1, 2.0f, {0}},
{COL_DATE, SORT_DATE, Localizable("Date"), 1, 150.0f, {0}},
{-1, -1, "", 1, s_ListBox.ScrollbarWidthMax(), {0}},
};
CUIRect Headers, ListBox;
ListView.HSplitTop(ms_ListheaderHeight, &Headers, &ListBox);
Headers.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_T, 5.0f);
ListBox.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 5.0f);
for(auto &Col : s_aCols)
{
if(Col.m_Direction == -1)
{
Headers.VSplitLeft(Col.m_Width, &Col.m_Rect, &Headers);
}
}
for(int i = std::size(s_aCols) - 1; i >= 0; i--)
{
if(s_aCols[i].m_Direction == 1)
{
Headers.VSplitRight(s_aCols[i].m_Width, &Headers, &s_aCols[i].m_Rect);
}
}
for(auto &Col : s_aCols)
{
if(Col.m_Direction == 0)
Col.m_Rect = Headers;
}
for(auto &Col : s_aCols)
{
if(Col.m_pCaption[0] != '\0' && Col.m_Sort != -1)
{
if(DoButton_GridHeader(&Col.m_Id, Localize(Col.m_pCaption), g_Config.m_BrDemoSort == Col.m_Sort, &Col.m_Rect))
{
if(g_Config.m_BrDemoSort == Col.m_Sort)
g_Config.m_BrDemoSortOrder ^= 1;
else
g_Config.m_BrDemoSortOrder = 0;
g_Config.m_BrDemoSort = Col.m_Sort;
// Don't rescan in order to keep fetched headers, just resort
std::stable_sort(m_vDemos.begin(), m_vDemos.end());
DemolistOnUpdate(false);
}
}
}
if(m_DemolistSelectedReveal)
{
s_ListBox.ScrollToSelected();
m_DemolistSelectedReveal = false;
}
s_ListBox.DoStart(ms_ListheaderHeight, m_vpFilteredDemos.size(), 1, 3, m_DemolistSelectedIndex, &ListBox, false, IGraphics::CORNER_ALL, true);
char aBuf[64];
int ItemIndex = -1;
for(auto &pItem : m_vpFilteredDemos)
{
ItemIndex++;
const CListboxItem ListItem = s_ListBox.DoNextItem(pItem, ItemIndex == m_DemolistSelectedIndex);
if(!ListItem.m_Visible)
continue;
for(const auto &Col : s_aCols)
{
CUIRect Button;
Button.x = Col.m_Rect.x;
Button.y = ListItem.m_Rect.y;
Button.h = ListItem.m_Rect.h;
Button.w = Col.m_Rect.w;
if(Col.m_Id == COL_ICON)
{
Button.Margin(1.0f, &Button);
const char *pIconType;
if(pItem->m_IsLink || str_comp(pItem->m_aFilename, "..") == 0)
pIconType = FONT_ICON_FOLDER_TREE;
else if(pItem->m_IsDir)
pIconType = FONT_ICON_FOLDER;
else
pIconType = FONT_ICON_FILM;
ColorRGBA IconColor;
if(!pItem->m_IsDir && (!pItem->m_InfosLoaded || !pItem->m_Valid))
IconColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); // not loaded
else
IconColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
TextRender()->TextColor(IconColor);
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
UI()->DoLabel(&Button, pIconType, 12.0f, TEXTALIGN_ML);
TextRender()->SetRenderFlags(0);
TextRender()->TextColor(TextRender()->DefaultTextColor());
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
}
else if(Col.m_Id == COL_DEMONAME)
{
SLabelProperties Props;
Props.m_MaxWidth = Button.w;
Props.m_EllipsisAtEnd = true;
Props.m_EnableWidthCheck = false;
UI()->DoLabel(&Button, pItem->m_aName, 12.0f, TEXTALIGN_ML, Props);
}
else if(Col.m_Id == COL_LENGTH && !pItem->m_IsDir && pItem->m_Valid)
{
str_time((int64_t)pItem->Length() * 100, TIME_HOURS, aBuf, sizeof(aBuf));
Button.VMargin(4.0f, &Button);
UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR);
}
else if(Col.m_Id == COL_DATE && !pItem->m_IsDir)
{
str_timestamp_ex(pItem->m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE);
Button.VMargin(4.0f, &Button);
UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR);
}
}
}
const int NewSelected = s_ListBox.DoEnd();
if(NewSelected != m_DemolistSelectedIndex)
{
m_DemolistSelectedIndex = NewSelected;
if(m_DemolistSelectedIndex >= 0)
str_copy(m_aCurrentDemoSelectionName, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
DemolistOnUpdate(false);
}
WasListboxItemActivated = s_ListBox.WasItemActivated();
}
void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView)
{
CUIRect Contents, Header;
DetailsView.HSplitTop(ms_ListheaderHeight, &Header, &Contents);
Contents.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 5.0f);
Contents.Margin(5.0f, &Contents);
const float FontSize = 12.0f;
CDemoItem *pItem = m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex] : nullptr;
Header.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_T, 5.0f);
const char *pHeaderLabel;
if(pItem == nullptr)
pHeaderLabel = Localize("No demo selected");
else if(str_comp(pItem->m_aFilename, "..") == 0)
pHeaderLabel = Localize("Parent Folder");
else if(pItem->m_IsLink)
pHeaderLabel = Localize("Folder Link");
else if(pItem->m_IsDir)
pHeaderLabel = Localize("Folder");
else if(!FetchHeader(*pItem))
pHeaderLabel = Localize("Invalid Demo");
else
pHeaderLabel = Localize("Demo");
UI()->DoLabel(&Header, pHeaderLabel, FontSize + 2.0f, TEXTALIGN_MC);
if(pItem == nullptr || pItem->m_IsDir)
return;
char aBuf[256];
CUIRect Left, Right;
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, Localize("Created"), FontSize, TEXTALIGN_ML);
str_timestamp_ex(pItem->m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE);
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML);
Contents.HSplitTop(4.0f, nullptr, &Contents);
if(!pItem->m_Valid)
return;
Contents.HSplitTop(18.0f, &Left, &Contents);
Left.VSplitMid(&Left, &Right, 4.0f);
UI()->DoLabel(&Left, Localize("Type"), FontSize, TEXTALIGN_ML);
UI()->DoLabel(&Right, Localize("Version"), FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
Left.VSplitMid(&Left, &Right, 4.0f);
UI()->DoLabel(&Left, pItem->m_Info.m_aType, FontSize - 1.0f, TEXTALIGN_ML);
str_from_int(pItem->m_Info.m_Version, aBuf);
UI()->DoLabel(&Right, aBuf, FontSize - 1.0f, TEXTALIGN_ML);
Contents.HSplitTop(4.0f, nullptr, &Contents);
Contents.HSplitTop(18.0f, &Left, &Contents);
Left.VSplitMid(&Left, &Right, 4.0f);
UI()->DoLabel(&Left, Localize("Length"), FontSize, TEXTALIGN_ML);
UI()->DoLabel(&Right, Localize("Markers"), FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
Left.VSplitMid(&Left, &Right, 4.0f);
str_time((int64_t)pItem->Length() * 100, TIME_HOURS, aBuf, sizeof(aBuf));
UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML);
str_from_int(pItem->NumMarkers(), aBuf);
UI()->DoLabel(&Right, aBuf, FontSize - 1.0f, TEXTALIGN_ML);
Contents.HSplitTop(4.0f, nullptr, &Contents);
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, Localize("Netversion"), FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, pItem->m_Info.m_aNetversion, FontSize - 1.0f, TEXTALIGN_ML);
Contents.HSplitTop(16.0f, nullptr, &Contents);
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, Localize("Map"), FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, pItem->m_Info.m_aMapName, FontSize - 1.0f, TEXTALIGN_ML);
Contents.HSplitTop(4.0f, nullptr, &Contents);
Contents.HSplitTop(18.0f, &Left, &Contents);
UI()->DoLabel(&Left, Localize("Size"), FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
const float Size = pItem->Size() / 1024.0f;
if(Size == 0.0f)
str_copy(aBuf, Localize("map not included", "Demo details"));
else if(Size > 1024)
str_format(aBuf, sizeof(aBuf), Localize("%.2f MiB"), Size / 1024.0f);
else
str_format(aBuf, sizeof(aBuf), Localize("%.2f KiB"), Size);
UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML);
Contents.HSplitTop(4.0f, nullptr, &Contents);
Contents.HSplitTop(18.0f, &Left, &Contents);
if(pItem->m_MapInfo.m_Sha256 != SHA256_ZEROED)
{
UI()->DoLabel(&Left, "SHA256", FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
char aSha[SHA256_MAXSTRSIZE];
sha256_str(pItem->m_MapInfo.m_Sha256, aSha, sizeof(aSha));
SLabelProperties Props;
Props.m_MaxWidth = Left.w;
Props.m_EllipsisAtEnd = true;
Props.m_EnableWidthCheck = false;
UI()->DoLabel(&Left, aSha, FontSize - 1.0f, TEXTALIGN_ML, Props);
}
else
{
UI()->DoLabel(&Left, "CRC32", FontSize, TEXTALIGN_ML);
Contents.HSplitTop(18.0f, &Left, &Contents);
str_format(aBuf, sizeof(aBuf), "%08x", pItem->m_MapInfo.m_Crc);
UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML);
}
Contents.HSplitTop(4.0f, nullptr, &Contents);
}
void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemActivated)
{
const auto &&SetIconMode = [&](bool Enable) {
if(Enable)
{
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
}
else
{
TextRender()->SetRenderFlags(0);
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
}
};
CUIRect ButtonBarTop, ButtonBarBottom;
ButtonsView.HSplitTop(5.0f, nullptr, &ButtonsView);
ButtonsView.HSplitMid(&ButtonBarTop, &ButtonBarBottom, 5.0f);
// quick search
{
SetIconMode(true);
CUIRect DemoSearch, SearchIcon;
ButtonBarTop.VSplitLeft(ButtonBarBottom.h * 21.0f, &DemoSearch, &ButtonBarTop);
ButtonBarTop.VSplitLeft(ButtonBarTop.h / 2.0f, nullptr, &ButtonBarTop);
DemoSearch.VSplitLeft(TextRender()->TextWidth(14.0f, FONT_ICON_MAGNIFYING_GLASS), &SearchIcon, &DemoSearch);
DemoSearch.VSplitLeft(5.0f, nullptr, &DemoSearch);
UI()->DoLabel(&SearchIcon, FONT_ICON_MAGNIFYING_GLASS, 14.0f, TEXTALIGN_ML);
SetIconMode(false);
m_DemoSearchInput.SetEmptyText(Localize("Search"));
if(Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed())
{
UI()->SetActiveItem(&m_DemoSearchInput);
m_DemoSearchInput.SelectAll();
}
if(UI()->DoClearableEditBox(&m_DemoSearchInput, &DemoSearch, 12.0f))
{
RefreshFilteredDemos();
DemolistOnUpdate(false);
}
}
// refresh button
{
CUIRect RefreshButton;
ButtonBarBottom.VSplitLeft(ButtonBarBottom.h * 3.0f, &RefreshButton, &ButtonBarBottom);
ButtonBarBottom.VSplitLeft(ButtonBarBottom.h / 2.0f, nullptr, &ButtonBarBottom);
SetIconMode(true);
static CButtonContainer s_RefreshButton;
if(DoButton_Menu(&s_RefreshButton, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &RefreshButton) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed()))
{
SetIconMode(false);
DemolistPopulate();
DemolistOnUpdate(false);
}
SetIconMode(false);
}
// fetch info checkbox
{
CUIRect FetchInfo;
ButtonBarBottom.VSplitLeft(ButtonBarBottom.h * 7.0f, &FetchInfo, &ButtonBarBottom);
ButtonBarBottom.VSplitLeft(ButtonBarBottom.h / 2.0f, nullptr, &ButtonBarBottom);
if(DoButton_CheckBox(&g_Config.m_BrDemoFetchInfo, Localize("Fetch Info"), g_Config.m_BrDemoFetchInfo, &FetchInfo))
{
g_Config.m_BrDemoFetchInfo ^= 1;
if(g_Config.m_BrDemoFetchInfo)
FetchAllHeaders();
}
}
// demos directory button
if(m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
{
CUIRect DemosDirectoryButton;
ButtonBarBottom.VSplitLeft(ButtonBarBottom.h * 10.0f, &DemosDirectoryButton, &ButtonBarBottom);
ButtonBarBottom.VSplitLeft(ButtonBarBottom.h / 2.0f, nullptr, &ButtonBarBottom);
static CButtonContainer s_DemosDirectoryButton;
if(DoButton_Menu(&s_DemosDirectoryButton, Localize("Demos directory"), 0, &DemosDirectoryButton))
{
char aBuf[IO_MAX_PATH_LENGTH];
Storage()->GetCompletePath(m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, aBuf, sizeof(aBuf));
if(!open_file(aBuf))
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
}
}
GameClient()->m_Tooltips.DoToolTip(&s_DemosDirectoryButton, &DemosDirectoryButton, Localize("Open the directory that contains the demo files"));
}
// play/open button
if(m_DemolistSelectedIndex >= 0)
{
CUIRect PlayButton;
ButtonBarBottom.VSplitRight(ButtonBarBottom.h * 3.0f, &ButtonBarBottom, &PlayButton);
ButtonBarBottom.VSplitRight(ButtonBarBottom.h, &ButtonBarBottom, nullptr);
SetIconMode(true);
static CButtonContainer s_PlayButton;
if(DoButton_Menu(&s_PlayButton, (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? FONT_ICON_FOLDER_OPEN : FONT_ICON_PLAY, 0, &PlayButton) || WasListboxItemActivated || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
{
SetIconMode(false);
if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder
{
m_DemoSearchInput.Clear();
const bool ParentFolder = str_comp(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, "..") == 0;
if(ParentFolder) // parent folder
{
str_copy(m_aCurrentDemoSelectionName, fs_filename(m_aCurrentDemoFolder));
str_append(m_aCurrentDemoSelectionName, "/");
if(fs_parent_dir(m_aCurrentDemoFolder))
{
m_aCurrentDemoFolder[0] = '\0';
if(m_DemolistStorageType == IStorage::TYPE_ALL)
{
m_aCurrentDemoSelectionName[0] = '\0'; // will select first list item
}
else
{
Storage()->GetCompletePath(m_DemolistStorageType, "demos", m_aCurrentDemoSelectionName, sizeof(m_aCurrentDemoSelectionName));
str_append(m_aCurrentDemoSelectionName, "/");
}
}
}
else // sub folder
{
if(m_aCurrentDemoFolder[0] != '\0')
str_append(m_aCurrentDemoFolder, "/");
else
m_DemolistStorageType = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType;
str_append(m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
}
DemolistPopulate();
DemolistOnUpdate(!ParentFolder);
}
else // file
{
char aBuf[IO_MAX_PATH_LENGTH];
str_format(aBuf, sizeof(aBuf), "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
const char *pError = Client()->DemoPlayer_Play(aBuf, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType);
m_LastPauseChange = -1.0f;
m_LastSpeedChange = -1.0f;
if(pError)
{
PopupMessage(Localize("Error loading demo"), pError, Localize("Ok"));
}
else
{
UI()->SetActiveItem(nullptr);
return;
}
}
}
SetIconMode(false);
if(m_aCurrentDemoFolder[0] != '\0')
{
if(str_comp(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, "..") != 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE)
{
// rename button
CUIRect RenameButton;
ButtonBarBottom.VSplitRight(ButtonBarBottom.h * 3.0f, &ButtonBarBottom, &RenameButton);
ButtonBarBottom.VSplitRight(ButtonBarBottom.h / 2.0f, &ButtonBarBottom, nullptr);
SetIconMode(true);
static CButtonContainer s_RenameButton;
if(DoButton_Menu(&s_RenameButton, FONT_ICON_PENCIL, 0, &RenameButton))
{
SetIconMode(false);
m_Popup = POPUP_RENAME_DEMO;
if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
{
m_DemoRenameInput.Set(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
}
else
{
char aNameWithoutExt[IO_MAX_PATH_LENGTH];
fs_split_file_extension(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, aNameWithoutExt, sizeof(aNameWithoutExt));
m_DemoRenameInput.Set(aNameWithoutExt);
}
UI()->SetActiveItem(&m_DemoRenameInput);
return;
}
// delete button
static CButtonContainer s_DeleteButton;
CUIRect DeleteButton;
ButtonBarBottom.VSplitRight(ButtonBarBottom.h * 3.0f, &ButtonBarBottom, &DeleteButton);
ButtonBarBottom.VSplitRight(ButtonBarBottom.h / 2.0f, &ButtonBarBottom, nullptr);
if(DoButton_Menu(&s_DeleteButton, FONT_ICON_TRASH, 0, &DeleteButton) || UI()->ConsumeHotkey(CUI::HOTKEY_DELETE) || (Input()->KeyPress(KEY_D) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
{
SetIconMode(false);
char aBuf[128 + IO_MAX_PATH_LENGTH];
str_format(aBuf, sizeof(aBuf), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize("Are you sure that you want to delete the folder '%s'?") : Localize("Are you sure that you want to delete the demo '%s'?"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
PopupConfirm(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize("Delete folder") : Localize("Delete demo"), aBuf, Localize("Yes"), Localize("No"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? &CMenus::PopupConfirmDeleteFolder : &CMenus::PopupConfirmDeleteDemo);
return;
}
SetIconMode(false);
}
#if defined(CONF_VIDEORECORDER)
// render demo button
if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
{
CUIRect RenderButton;
ButtonBarTop.VSplitRight(ButtonBarBottom.h * 3.0f, &ButtonBarTop, &RenderButton);
ButtonBarTop.VSplitRight(ButtonBarBottom.h, &ButtonBarTop, nullptr);
SetIconMode(true);
static CButtonContainer s_RenderButton;
if(DoButton_Menu(&s_RenderButton, FONT_ICON_VIDEO, 0, &RenderButton) || (Input()->KeyPress(KEY_R) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
{
SetIconMode(false);
m_Popup = POPUP_RENDER_DEMO;
m_StartPaused = false;
char aNameWithoutExt[IO_MAX_PATH_LENGTH];
fs_split_file_extension(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, aNameWithoutExt, sizeof(aNameWithoutExt));
m_DemoRenderInput.Set(aNameWithoutExt);
UI()->SetActiveItem(&m_DemoRenderInput);
return;
}
SetIconMode(false);
}
#endif
}
}
}
void CMenus::PopupConfirmDeleteDemo()
{
char aBuf[IO_MAX_PATH_LENGTH];
str_format(aBuf, sizeof(aBuf), "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
if(Storage()->RemoveFile(aBuf, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
{
DemolistPopulate();
DemolistOnUpdate(false);
}
else
{
char aError[128 + IO_MAX_PATH_LENGTH];
str_format(aError, sizeof(aError), Localize("Unable to delete the demo '%s'"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
PopupMessage(Localize("Error"), aError, Localize("Ok"));
}
}
void CMenus::PopupConfirmDeleteFolder()
{
char aBuf[IO_MAX_PATH_LENGTH];
str_format(aBuf, sizeof(aBuf), "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
if(Storage()->RemoveFolder(aBuf, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
{
DemolistPopulate();
DemolistOnUpdate(false);
}
else
{
char aError[128 + IO_MAX_PATH_LENGTH];
str_format(aError, sizeof(aError), Localize("Unable to delete the folder '%s'. Make sure it's empty first."), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
PopupMessage(Localize("Error"), aError, Localize("Ok"));
}
}