ddnet/src/game/client/components/menus_demo.cpp

1476 lines
51 KiB
C++
Raw Normal View History

2010-11-20 10:37:14 +00:00
/* (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. */
2019-10-14 00:27:08 +00:00
#include <base/hash.h>
#include <base/math.h>
2022-05-18 16:00:05 +00:00
#include <base/system.h>
2010-05-29 07:25:38 +00:00
#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>
2010-05-29 07:25:38 +00:00
#include <game/client/ui.h>
#include <game/client/ui_listbox.h>
#include <game/generated/client_data.h>
#include <game/localization.h>
#include "maplayers.h"
2010-05-29 07:25:38 +00:00
#include "menus.h"
2022-05-18 16:00:05 +00:00
#include <chrono>
using namespace FontIcons;
2022-05-18 16:00:05 +00:00
using namespace std::chrono_literals;
2022-08-19 05:05:02 +00:00
int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, bool Enabled)
2022-08-18 00:28:24 +00:00
{
pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, (Checked ? 0.10f : 0.5f) * UI()->ButtonColorMul(pButtonContainer)), Corners, 5.0f);
2022-08-17 07:58:13 +00:00
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
2023-08-21 21:21:48 +00:00
TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
2022-08-18 00:28:24 +00:00
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
TextRender()->TextColor(TextRender()->DefaultTextColor());
2022-08-17 07:58:13 +00:00
CUIRect Temp;
pRect->HMargin(2.0f, &Temp);
UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC);
2022-08-18 00:28:24 +00:00
2022-08-24 20:17:04 +00:00
if(!Enabled)
{
2022-08-18 00:28:24 +00:00
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);
2022-08-18 00:28:24 +00:00
TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor());
TextRender()->TextColor(TextRender()->DefaultTextColor());
}
2023-08-21 21:21:48 +00:00
TextRender()->SetRenderFlags(0);
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
2022-07-16 13:32:06 +00:00
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_KillMessages.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();
}
2010-05-29 07:25:38 +00:00
void CMenus::RenderDemoPlayer(CUIRect MainView)
{
const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
// 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();
};
2019-04-15 18:39:39 +00:00
// handle keyboard shortcuts independent of active menu
float PositionToSeek = -1.0f;
float TimeToSeek = 0.0f;
2021-07-12 09:43:56 +00:00
if(m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts)
2019-04-15 18:39:39 +00:00
{
// increase/decrease speed
if(!Input()->ShiftIsPressed())
2019-04-15 18:39:39 +00:00
{
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();
}
2019-04-15 18:39:39 +00:00
}
// pause/unpause
if(Input()->KeyPress(KEY_SPACE) || Input()->KeyPress(KEY_RETURN) || Input()->KeyPress(KEY_K))
{
if(pInfo->m_Paused)
{
DemoPlayer()->Unpause();
}
else
{
DemoPlayer()->Pause();
}
UpdateLastPauseChange();
}
2019-04-15 18:39:39 +00:00
// seek backward/forward 10/5 seconds
if(Input()->KeyPress(KEY_J))
2019-04-15 18:39:39 +00:00
{
TimeToSeek = -10.0f;
}
else if(Input()->KeyPress(KEY_L))
{
TimeToSeek = 10.0f;
}
else if(Input()->KeyPress(KEY_LEFT))
{
TimeToSeek = -5.0f;
}
else if(Input()->KeyPress(KEY_RIGHT))
{
TimeToSeek = 5.0f;
2019-04-15 18:39:39 +00:00
}
// 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);
}
2019-04-15 18:39:39 +00:00
}
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;
}
const int CurrentTick = pInfo->m_CurrentTick - pInfo->m_FirstTick;
const int TotalTicks = pInfo->m_LastTick - pInfo->m_FirstTick;
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;
2022-11-20 04:00:20 +00:00
CUIRect SeekBar, ButtonBar, NameBar, SpeedBar;
DemoControls.Margin(5.0f, &DemoControls);
DemoControls.HSplitTop(SeekBarHeight, &SeekBar, &ButtonBar);
ButtonBar.HSplitTop(Margins, nullptr, &ButtonBar);
2014-01-15 14:08:00 +00:00
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;
2010-05-29 07:25:38 +00:00
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;
2010-05-29 07:25:38 +00:00
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
2020-10-18 21:33:45 +00:00
char aCurrentTime[32];
2021-06-23 05:05:49 +00:00
str_time((int64_t)CurrentTick / SERVER_TICK_SPEED * 100, TIME_HOURS, aCurrentTime, sizeof(aCurrentTime));
2020-10-18 21:33:45 +00:00
char aTotalTime[32];
2021-06-23 05:05:49 +00:00
str_time((int64_t)TotalTicks / SERVER_TICK_SPEED * 100, TIME_HOURS, aTotalTime, sizeof(aTotalTime));
2020-10-18 21:33:45 +00:00
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))
{
2009-10-27 14:38:53 +00:00
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);
2014-01-22 16:14:47 +00:00
if(Input()->ShiftIsPressed())
2014-01-22 16:14:47 +00:00
{
AmountSeek = s_PrevAmount + (AmountSeek - s_PrevAmount) * 0.05f;
if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(s_PrevAmount - AmountSeek) >= 0.0001f)
2014-01-22 16:14:47 +00:00
{
PositionToSeek = AmountSeek;
2014-01-22 16:14:47 +00:00
}
}
else
{
if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(s_PrevAmount - AmountSeek) >= 0.001f)
2014-01-22 16:14:47 +00:00
{
s_PrevAmount = AmountSeek;
PositionToSeek = AmountSeek;
2014-01-22 16:14:47 +00:00
}
}
}
}
else if(UI()->HotItem() == pId)
{
2009-10-27 14:38:53 +00:00
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 / SERVER_TICK_SPEED * 100, TIME_HOURS, s_aHoveredTime, sizeof(s_aHoveredTime));
GameClient()->m_Tooltips.DoToolTip(pId, &SeekBar, s_aHoveredTime);
}
}
2010-05-29 07:25:38 +00:00
if(Inside)
UI()->SetHotItem(pId);
}
bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false;
2014-01-15 14:08:00 +00:00
// do buttons
CUIRect Button;
2014-01-15 14:08:00 +00:00
// combined play and pause button
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_PlayPauseButton;
if(DoButton_FontIcon(&s_PlayPauseButton, pInfo->m_Paused ? FONT_ICON_PLAY : FONT_ICON_PAUSE, false, &Button, IGraphics::CORNER_ALL))
2014-01-15 14:08:00 +00:00
{
2019-04-15 18:39:39 +00:00
if(pInfo->m_Paused)
{
2014-01-15 14:08:00 +00:00
DemoPlayer()->Unpause();
2019-04-15 18:39:39 +00:00
}
else
{
DemoPlayer()->Pause();
}
UpdateLastPauseChange();
2014-01-15 14:08:00 +00:00
}
2022-08-26 06:07:58 +00:00
GameClient()->m_Tooltips.DoToolTip(&s_PlayPauseButton, &Button, pInfo->m_Paused ? Localize("Play the current demo") : Localize("Pause the current demo"));
2014-01-15 14:08:00 +00:00
// stop button
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
2014-01-15 14:08:00 +00:00
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_ResetButton;
if(DoButton_FontIcon(&s_ResetButton, FONT_ICON_STOP, false, &Button, IGraphics::CORNER_ALL))
2014-01-15 14:08:00 +00:00
{
DemoPlayer()->Pause();
PositionToSeek = 0.0f;
}
2022-08-26 06:07:58 +00:00
GameClient()->m_Tooltips.DoToolTip(&s_ResetButton, &Button, Localize("Stop the current demo"));
// one tick back
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins + 10.0f, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneTickBackButton;
if(DoButton_FontIcon(&s_OneTickBackButton, FONT_ICON_CHEVRON_LEFT, 0, &Button, IGraphics::CORNER_ALL))
DemoSeekTick(IDemoPlayer::TICK_PREVIOUS);
GameClient()->m_Tooltips.DoToolTip(&s_OneTickBackButton, &Button, Localize("Go back one tick"));
// one tick forward
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
2014-01-15 14:08:00 +00:00
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneTickForwardButton;
if(DoButton_FontIcon(&s_OneTickForwardButton, FONT_ICON_CHEVRON_RIGHT, 0, &Button, IGraphics::CORNER_ALL))
DemoSeekTick(IDemoPlayer::TICK_NEXT);
GameClient()->m_Tooltips.DoToolTip(&s_OneTickForwardButton, &Button, Localize("Go forward one tick"));
// slowdown
ButtonBar.VSplitLeft(Margins + 10.0f, 0, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_SlowDownButton;
if(DoButton_FontIcon(&s_SlowDownButton, FONT_ICON_BACKWARD, 0, &Button, IGraphics::CORNER_ALL))
2014-01-15 14:08:00 +00:00
DecreaseDemoSpeed = true;
2022-08-26 06:07:58 +00:00
GameClient()->m_Tooltips.DoToolTip(&s_SlowDownButton, &Button, Localize("Slow down the demo"));
2014-01-15 14:08:00 +00:00
// fastforward
ButtonBar.VSplitLeft(Margins, 0, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_FastForwardButton;
if(DoButton_FontIcon(&s_FastForwardButton, FONT_ICON_FORWARD, 0, &Button, IGraphics::CORNER_ALL))
2014-01-15 14:08:00 +00:00
IncreaseDemoSpeed = true;
2022-08-26 06:07:58 +00:00
GameClient()->m_Tooltips.DoToolTip(&s_FastForwardButton, &Button, Localize("Speed up the demo"));
2014-01-15 14:08:00 +00:00
// speed meter
ButtonBar.VSplitLeft(Margins * 12, &SpeedBar, &ButtonBar);
2014-01-15 14:08:00 +00:00
char aBuffer[64];
2017-03-22 20:10:53 +00:00
str_format(aBuffer, sizeof(aBuffer), "×%g", pInfo->m_Speed);
UI()->DoLabel(&SpeedBar, aBuffer, Button.h * 0.7f, TEXTALIGN_MC);
2014-01-15 14:08:00 +00:00
// slice begin button
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_SliceBeginButton;
const int SliceBeginButtonResult = DoButton_FontIcon(&s_SliceBeginButton, FONT_ICON_RIGHT_FROM_BRACKET, 0, &Button, IGraphics::CORNER_ALL);
if(SliceBeginButtonResult == 1)
{
2022-11-22 01:19:58 +00:00
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
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_SliceEndButton;
const int SliceEndButtonResult = DoButton_FontIcon(&s_SliceEndButton, FONT_ICON_RIGHT_TO_BRACKET, 0, &Button, IGraphics::CORNER_ALL);
if(SliceEndButtonResult == 1)
{
2022-11-22 01:19:58 +00:00
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
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
2022-07-16 13:32:06 +00:00
static CButtonContainer s_SliceSaveButton;
if(DoButton_FontIcon(&s_SliceSaveButton, FONT_ICON_ARROW_UP_RIGHT_FROM_SQUARE, 0, &Button, IGraphics::CORNER_ALL))
2014-08-23 15:48:04 +00:00
{
Port line input and IME support from 0.7 Port the line input (UI edit boxes, chat, console) and Input Method Editor (IME) support from upstream. Closes #4397. General ------------------------------ Fix issues with the text input. Closes #4346. Closes #4524. Word skipping (when holding Ctrl) is overhauled to be consistent with the Windows / Firefox experience that I took as reference. Improve usability by not blinking (i.e. always rendering) the caret shortly after is has been moved. UI text input ------------------------------ Fix inconsistent mouse-based left and right scrolling (closes #4347). Support smooth left and right scrolling. Chat ------------------------------ Support keyboard-based text selection of the chat input. Mouse-based selection could be support in the future when we decide to add something like an ingame UI cursor. Support smooth up and down scrolling of the chat input, removing the old hack that offsets the input string to simulate scrolling. Console ------------------------------ Also support mouse-based text selection of the command input. Only text from either the command input or the console log can be selected at the same time. This ensures that Ctrl+C will always copy the text that is currently visually selected in the console. Check for Ctrl+C input event in event handler instead of in render function, to hopefully fix the issue that copying does not work sometimes (closes #5974 until further notice). When Ctrl+C is used to copy text from the console log, the selection is cleared. This should make it more clear when text was copied from the log. Fix an issue that was preventing the console log selection from being cleared, when all log lines are selected. Remove Ctrl+A/E hotkeys that move cursor to beginning/end respectively. Ctrl+A now selectes all text like for all other inputs. Home and End keys can still be used to go the beginning and end. Remove Ctrl+U/K hotkeys that clear everything before/after the cursor respectively. Hold shift and use Home/End to select everything instead. IME support ------------------------------ Render list of IME candidates in the client on Windows, so the candidate list can also be viewed in fullscreen mode. There is no API available to retrieve a candidate list on the other operating systems. Improve composition rendering by underlining the composition text instead of putting it in square brackets. Track active input globally to properly activate and deactivate IME through the SDL functions. Closes #1030. Closes #1008. Password rendering ------------------------------ Fix rendering of passwords containing unicode. Instead of rendering one star character for each UTF-8 `char`, render on star for every unicode codepoint. Show the composition text also for passwords. Without seeing the composition text it's hard to type a password containing those characters. The candidate window exposes the composition anyway. If you don't want to expose your password this way, e.g. while streaming, you could: 1. Use a latin password and switch off the IME for the password input with the IME hotkey. 2. Blank your screen with an external program while you are streaming and entering passwords. 3. Use binds to authenticate in rcon or to set the server browser password. Refactoring ------------------------------ Move all text input logic and general rendering to `CLineInput`. A `CLineInput` is associated with a particular `char` buffer given as a pointer either in the constructor or with `SetBuffer`. The maximum byte size of the buffer must also be specified. The maximum length in unicode codepoints can also be specified separately (e.g. on upstream, name are limited by the number of unicode codepoints instead). Add `CLineInputBuffered`, which is a `CLineInput` that own a `char` buffer of a fixed size, which is specified as a template argument. As `CLineInput` does not own a buffer anymore, this reduces duplicate code for line inputs that need their own buffer. Add `CLineInputNumber` which has additional convenience functions to consider the text as an `int` or `float`, to reduce duplicate code in those cases. In the future we could also add an input filter function so that only numbers can be entered in the number input. Add `CLineInput::SetClipboardLineCallback` to handle the case that multiple lines of text are pasted into a lineinput. This reduces duplicate code, as this behavior was previously implemented separately for chat and console. The behavior is also fixed to be consistent with the console on Windows, so the first line being pasted edits the current input text and then sends it instead of being sent on its own without the existing input text. Add `CalcFontSizeAndBoundingBox` to UI to reduce duplicate code. Expose `CalcAlignedCursorPos` as static member function to reuse it for line input. Dispatch input events to UI inputs through the event handler instead of storing them in a duplicate buffer. Use `size_t` for line input cursor position, length etc. and for `str_utf8_stats`. Add `IButtonColorFunction` to UI to describe a functions that defines colors for the Default, Active and Hovered states of UI elements. Add some default button color functions. Use button color function to reduce duplicate code in scrollbar rendering. Use `vec2` instead of two `floats` to represent the mouse positions in the text renderer. Remove `CaretPosition` again, as it does not calculate the correct Y position near line breaks due to the wrapping being different when not rendering the entire string. Instead, calculate the exact caret position when rending a text container and store the caret position in the text cursor for later use. IME usage guide (Windows) ------------------------------ 1. Install the respective language and the Microsoft-IME keyboard (e.g. for Chinese, Japanese or Korean). 2. Launch the game (or a text editor to first try out the IME). Note that Windows may track the input language separately for every application. You can change this in the Windows input settings so the input language is changed globally. 2. Switch the input language using the hotkey Windows+Space or another hotkey that you configured in the Windows input settings (Alt+Shift is the default, but you should consider disabling it, to avoid accidentally changing the input language while playing). 3. Switch from Latin/English input mode to the respective asian input mode. - Chinese: Use Ctrl+Space to switch between English and Chinese input mode. You can change this hotkey in the IME's settings. - Japanese: Use Ctrl+Space to switch between Alphanumeric and Hiragana/Katakana input mode. You can change this hotkey in the IME's settings. - Korean: Use Right Alt to switch between English and Hangul input mode. You cannot change this hotkey as of yet. - Note that the input mode is also tracked per application, but there is no setting to change this behavior as far as I know, so you'll need to switch for every application separately. 4. Start typing. The underlined text is the current composition text. While a composition is active, you can only edit the composition text. Confirm the composition with Space or by selecting a candidate from the candidate list with the arrow keys. Cancel the composition with Escape or by using Backspace to delete the composition text. Note that not all languages offer a candidate list. SDL version-specific issues ------------------------------ - 2.26.5, 2.24.2, 2.0.22: IME candidates work. But there are minor bugs when moving the composition cursor. - 2.0.18, 2.0.20: IME candidates work. - 2.0.16 (our current version): IME candidates cannot be determined with Windows API. Windows tries to draw the composition window like before, so this does not work in fullscreen mode. - 2.0.8 (upstream 0.7): IME candidates work. But this SDL version is too old for us.
2023-01-03 21:28:38 +00:00
char aDemoName[IO_MAX_PATH_LENGTH];
DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName));
m_DemoSliceInput.Set(aDemoName);
m_DemoSliceInput.Append(".demo");
UI()->SetActiveItem(&m_DemoSliceInput);
2014-08-23 15:48:04 +00:00
m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE;
}
2022-09-29 16:54:04 +00:00
GameClient()->m_Tooltips.DoToolTip(&s_SliceSaveButton, &Button, Localize("Export cut as a separate demo"));
// threshold value, accounts for slight inaccuracy when setting demo position
const int Threshold = 10;
// one marker back
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins + 20.0f, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneMarkerBackButton;
if(DoButton_FontIcon(&s_OneMarkerBackButton, FONT_ICON_BACKWARD_STEP, 0, &Button, IGraphics::CORNER_ALL))
{
PositionToSeek = 0.0f;
2022-11-19 23:32:47 +00:00
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)
{
PositionToSeek = (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
break;
}
}
}
GameClient()->m_Tooltips.DoToolTip(&s_OneMarkerBackButton, &Button, Localize("Go back one marker"));
// one marker forward
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitLeft(Margins, nullptr, &ButtonBar);
ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar);
static CButtonContainer s_OneMarkerForwardButton;
if(DoButton_FontIcon(&s_OneMarkerForwardButton, FONT_ICON_FORWARD_STEP, 0, &Button, IGraphics::CORNER_ALL))
{
PositionToSeek = 1.0f;
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)
{
PositionToSeek = (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
break;
}
}
}
GameClient()->m_Tooltips.DoToolTip(&s_OneMarkerForwardButton, &Button, Localize("Go forward one marker"));
2014-01-15 14:08:00 +00:00
// 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))
{
2014-01-15 14:08:00 +00:00
Client()->Disconnect();
DemolistOnUpdate(false);
}
GameClient()->m_Tooltips.DoToolTip(&s_ExitButton, &Button, Localize("Close the demo player"));
2014-01-15 14:08:00 +00:00
// toggle keyboard shortcuts button
2023-05-28 10:51:56 +00:00
ButtonBar.VSplitRight(Margins, &ButtonBar, nullptr);
ButtonBar.VSplitRight(ButtonbarHeight, &ButtonBar, &Button);
2022-07-16 13:32:06 +00:00
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;
}
2022-08-26 06:07:58 +00:00
GameClient()->m_Tooltips.DoToolTip(&s_KeyboardShortcutsButton, &Button, Localize("Toggle keyboard shortcuts"));
2014-01-15 14:08:00 +00:00
// demo name
char aDemoName[IO_MAX_PATH_LENGTH];
2014-01-15 14:08:00 +00:00
DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName));
char aBuf[IO_MAX_PATH_LENGTH + 128];
2014-01-15 14:08:00 +00:00
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);
2014-01-15 14:08:00 +00:00
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 / SERVER_TICK_SPEED * 100, TIME_HOURS, aSliceBegin, sizeof(aSliceBegin));
char aSliceEnd[32];
str_time(RealSliceEnd / SERVER_TICK_SPEED * 100, TIME_HOURS, aSliceEnd, sizeof(aSliceEnd));
char aSliceLength[32];
str_time((RealSliceEnd - RealSliceBegin) / SERVER_TICK_SPEED * 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 RemoveChatCheckBox;
Box.HSplitTop(24.0f, &RemoveChatCheckBox, &Box);
Box.HSplitTop(20.0f, nullptr, &Box);
if(DoButton_CheckBox(&s_RemoveChat, Localize("Remove chat"), s_RemoveChat, &RemoveChatCheckBox))
{
s_RemoveChat ^= 1;
}
// 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];
DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName));
str_append(aDemoName, ".demo");
if(!str_endswith(m_DemoSliceInput.GetString(), ".demo"))
m_DemoSliceInput.Append(".demo");
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 name"));
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", 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", 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';
m_DemoPlayerState = DEMOPLAYER_NONE;
Client()->DemoSlice(aPath, CMenus::DemoFilterChat, &s_RemoveChat);
DemolistPopulate();
DemolistOnUpdate(false);
}
if(s_ConfirmPopupContext.m_Result != CUI::SConfirmPopupContext::UNSET)
{
s_ConfirmPopupContext.Reset();
}
}
int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
{
2010-09-18 13:08:57 +00:00
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;
}
2010-05-29 07:25:38 +00:00
CDemoItem Item;
2022-07-09 16:14:56 +00:00
str_copy(Item.m_aFilename, pInfo->m_pName);
if(IsDir)
{
str_format(Item.m_aName, sizeof(Item.m_aName), "%s/", pInfo->m_pName);
2019-04-08 20:13:47 +00:00
Item.m_InfosLoaded = false;
Item.m_Valid = false;
2020-10-08 05:19:04 +00:00
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_InfosLoaded = false;
Item.m_Date = pInfo->m_TimeModified;
}
Item.m_IsDir = IsDir != 0;
Item.m_IsLink = false;
2010-10-06 21:07:35 +00:00
Item.m_StorageType = StorageType;
pSelf->m_vDemos.push_back(Item);
if(time_get_nanoseconds() - pSelf->m_DemoPopulateStartTime > 500ms)
{
pSelf->GameClient()->m_Menus.RenderLoading(Localize("Loading demo files"), "", 0, false);
}
return 0;
}
2010-05-29 07:25:38 +00:00
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());
}
2023-08-26 11:13:12 +00:00
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)
m_aCurrentDemoSelectionName[0] = '\0';
2015-08-27 12:57:56 +00:00
else
{
bool Found = false;
int SelectedIndex = -1;
// search for selected index
2023-08-26 11:13:12 +00:00
for(auto &Item : m_vpFilteredDemos)
2015-08-27 12:57:56 +00:00
{
SelectedIndex++;
2023-08-26 11:13:12 +00:00
if(str_comp(m_aCurrentDemoSelectionName, Item->m_aName) == 0)
2015-08-27 12:57:56 +00:00
{
Found = true;
break;
}
}
2023-08-26 11:13:12 +00:00
RefreshFilteredDemos();
2015-08-27 12:57:56 +00:00
if(Found)
2015-08-27 12:57:56 +00:00
m_DemolistSelectedIndex = SelectedIndex;
}
2023-08-26 11:13:12 +00:00
m_DemolistSelectedIndex = Reset ? !m_vpFilteredDemos.empty() ? 0 : -1 :
m_DemolistSelectedIndex >= (int)m_vpFilteredDemos.size() ? m_vpFilteredDemos.size() - 1 : m_DemolistSelectedIndex;
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);
2019-10-14 00:27:08 +00:00
Item.m_Valid = DemoPlayer()->GetDemoInfo(Storage(), 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());
}
2010-05-29 07:25:38 +00:00
void CMenus::RenderDemoList(CUIRect MainView)
{
2010-05-29 07:25:38 +00:00
static int s_Inited = 0;
if(!s_Inited)
{
2010-05-29 07:25:38 +00:00
DemolistPopulate();
DemolistOnUpdate(true);
s_Inited = 1;
2010-09-05 17:06:27 +00:00
}
2010-10-13 10:59:30 +00:00
char aFooterLabel[128] = {0};
if(m_DemolistSelectedIndex >= 0)
{
2023-08-26 18:29:58 +00:00
CDemoItem *pItem = m_vpFilteredDemos[m_DemolistSelectedIndex];
2023-08-26 15:28:35 +00:00
if(str_comp(pItem->m_aFilename, "..") == 0)
2022-07-09 16:14:56 +00:00
str_copy(aFooterLabel, Localize("Parent Folder"));
2023-08-26 11:13:12 +00:00
else if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsLink)
str_copy(aFooterLabel, Localize("Folder Link"));
2023-08-26 11:13:12 +00:00
else if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
2022-07-09 16:14:56 +00:00
str_copy(aFooterLabel, Localize("Folder"));
2023-08-26 15:28:35 +00:00
else if(!FetchHeader(*pItem))
2022-07-09 16:14:56 +00:00
str_copy(aFooterLabel, Localize("Invalid Demo"));
2010-10-13 10:59:30 +00:00
else
2022-07-09 16:14:56 +00:00
str_copy(aFooterLabel, Localize("Demo details"));
}
// render background
MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f);
2010-05-29 07:25:38 +00:00
MainView.Margin(10.0f, &MainView);
2019-09-27 07:22:50 +00:00
#if defined(CONF_VIDEORECORDER)
2020-09-29 01:23:39 +00:00
CUIRect RenderRect;
2019-09-27 07:22:50 +00:00
#endif
2020-09-29 01:23:39 +00:00
CUIRect ButtonBar, RefreshRect, FetchRect, PlayRect, DeleteRect, RenameRect, LabelRect, ListBox;
2023-08-25 23:55:30 +00:00
CUIRect ButtonBar2, DirectoryButton, DemoSearch;
2020-09-29 01:23:39 +00:00
MainView.HSplitBottom((ms_ButtonHeight + 5.0f) * 2.0f, &MainView, &ButtonBar2);
ButtonBar2.HSplitTop(5.0f, 0, &ButtonBar2);
ButtonBar2.HSplitTop(ms_ButtonHeight, &ButtonBar2, &ButtonBar);
2010-05-29 07:25:38 +00:00
ButtonBar.HSplitTop(5.0f, 0, &ButtonBar);
2020-09-29 01:23:39 +00:00
ButtonBar2.VSplitLeft(110.0f, &FetchRect, &ButtonBar2);
ButtonBar2.VSplitLeft(10.0f, 0, &ButtonBar2);
ButtonBar2.VSplitLeft(230.0f, &DirectoryButton, &ButtonBar2);
ButtonBar2.VSplitLeft(10.0f, 0, &ButtonBar2);
2023-08-25 23:55:30 +00:00
ButtonBar2.VSplitLeft(230.0f, &DemoSearch, &ButtonBar2);
ButtonBar2.VSplitLeft(10.0f, 0, &ButtonBar2);
ButtonBar.VSplitRight(110.0f, &ButtonBar, &PlayRect);
ButtonBar.VSplitLeft(110.0f, &RefreshRect, &ButtonBar);
ButtonBar.VSplitLeft(10.0f, 0, &ButtonBar);
ButtonBar.VSplitLeft(110.0f, &DeleteRect, &ButtonBar);
ButtonBar.VSplitLeft(10.0f, 0, &ButtonBar);
ButtonBar.VSplitLeft(110.0f, &RenameRect, &ButtonBar);
ButtonBar.VSplitLeft(10.0f, 0, &ButtonBar);
2019-09-27 07:22:50 +00:00
#if defined(CONF_VIDEORECORDER)
2020-09-29 01:23:39 +00:00
ButtonBar2.VSplitRight(110.0f, &ButtonBar2, &RenderRect);
ButtonBar2.VSplitRight(10.0f, &ButtonBar2, 0);
2019-09-27 07:22:50 +00:00
#endif
2019-09-30 13:03:37 +00:00
ButtonBar.VSplitLeft(110.0f, &LabelRect, &ButtonBar);
MainView.HSplitBottom(140.0f, &ListBox, &MainView);
// render demo info
MainView.VMargin(5.0f, &MainView);
MainView.HSplitBottom(5.0f, &MainView, 0);
MainView.Draw(ColorRGBA(0, 0, 0, 0.15f), IGraphics::CORNER_B, 4.0f);
2023-08-26 11:13:12 +00:00
if(m_DemolistSelectedIndex >= 0 && !m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_Valid)
{
CUIRect Left, Right, Labels;
MainView.VMargin(20.0f, &MainView);
MainView.HMargin(10.0f, &MainView);
MainView.VSplitMid(&Labels, &MainView);
// left side
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Created:"), 14.0f, TEXTALIGN_ML);
2015-08-27 13:02:55 +00:00
char aTimestamp[256];
2023-08-26 11:13:12 +00:00
str_timestamp_ex(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_Date, aTimestamp, sizeof(aTimestamp), FORMAT_SPACE);
2015-08-27 13:02:55 +00:00
UI()->DoLabel(&Right, aTimestamp, 14.0f, TEXTALIGN_ML);
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Type:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
UI()->DoLabel(&Right, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_Info.m_aType, 14.0f, TEXTALIGN_ML);
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Length:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
int Length = m_vpFilteredDemos[m_DemolistSelectedIndex]->Length();
char aBuf[64];
2021-06-23 05:05:49 +00:00
str_time((int64_t)Length * 100, TIME_HOURS, aBuf, sizeof(aBuf));
UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML);
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Version:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
str_from_int(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_Info.m_Version, aBuf);
UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML);
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Markers:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
str_from_int(m_vpFilteredDemos[m_DemolistSelectedIndex]->NumMarkers(), aBuf);
UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML);
// right side
Labels = MainView;
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Map:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
UI()->DoLabel(&Right, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_Info.m_aMapName, 14.0f, TEXTALIGN_ML);
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Size:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
const float Size = m_vpFilteredDemos[m_DemolistSelectedIndex]->Size() / 1024.0f;
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(&Right, aBuf, 14.0f, TEXTALIGN_ML);
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
Left.VSplitLeft(150.0f, &Left, &Right);
2023-08-26 11:13:12 +00:00
if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_MapInfo.m_Sha256 != SHA256_ZEROED)
2019-12-17 14:44:54 +00:00
{
UI()->DoLabel(&Left, "SHA256:", 14.0f, TEXTALIGN_ML);
2019-12-17 14:44:54 +00:00
char aSha[SHA256_MAXSTRSIZE];
2023-08-26 11:13:12 +00:00
sha256_str(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_MapInfo.m_Sha256, aSha, sizeof(aSha) / 2);
UI()->DoLabel(&Right, aSha, Right.w > 235 ? 14.0f : 11.0f, TEXTALIGN_ML);
2019-12-17 14:44:54 +00:00
}
else
{
UI()->DoLabel(&Left, Localize("Crc:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
str_format(aBuf, sizeof(aBuf), "%08x", m_vpFilteredDemos[m_DemolistSelectedIndex]->m_MapInfo.m_Crc);
UI()->DoLabel(&Right, aBuf, 14.0f, TEXTALIGN_ML);
}
Labels.HSplitTop(5.0f, 0, &Labels);
Labels.HSplitTop(20.0f, &Left, &Labels);
2019-12-17 14:44:54 +00:00
Left.VSplitLeft(150.0f, &Left, &Right);
UI()->DoLabel(&Left, Localize("Netversion:"), 14.0f, TEXTALIGN_ML);
2023-08-26 11:13:12 +00:00
UI()->DoLabel(&Right, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_Info.m_aNetversion, 14.0f, TEXTALIGN_ML);
}
2015-08-25 13:55:15 +00:00
// demo list
CUIRect Headers;
ListBox.HSplitTop(ms_ListheaderHeight, &Headers, &ListBox);
struct CColumn
{
int m_ID;
int m_Sort;
CLocConstString m_Caption;
int m_Direction;
float m_Width;
CUIRect m_Rect;
};
enum
{
COL_ICON = 0,
COL_DEMONAME,
COL_MARKERS,
COL_LENGTH,
2015-08-25 13:55:15 +00:00
COL_DATE,
};
static CListBox s_ListBox;
2015-08-25 13:55:15 +00:00
static CColumn 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_MARKERS, SORT_MARKERS, Localizable("Markers"), 1, 75.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, 160.0f, {0}},
{-1, -1, "", 1, s_ListBox.ScrollbarWidthMax(), {0}},
2015-08-25 13:55:15 +00:00
};
Headers.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_NONE, 0.0f);
2015-08-25 13:55:15 +00:00
// do layout
for(auto &Col : s_aCols)
2015-08-25 13:55:15 +00:00
{
if(Col.m_Direction == -1)
2015-08-25 13:55:15 +00:00
{
Headers.VSplitLeft(Col.m_Width, &Col.m_Rect, &Headers);
2015-08-25 13:55:15 +00:00
}
}
for(int i = std::size(s_aCols) - 1; i >= 0; i--)
2015-08-25 13:55:15 +00:00
{
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)
2015-08-25 13:55:15 +00:00
{
if(Col.m_Direction == 0)
Col.m_Rect = Headers;
2015-08-25 13:55:15 +00:00
}
// do headers
for(auto &Col : s_aCols)
2015-08-25 13:55:15 +00:00
{
if(DoButton_GridHeader(&Col.m_ID, Col.m_Caption, g_Config.m_BrDemoSort == Col.m_Sort, &Col.m_Rect))
2015-08-25 13:55:15 +00:00
{
if(Col.m_Sort != -1)
2015-08-25 13:55:15 +00:00
{
if(g_Config.m_BrDemoSort == Col.m_Sort)
2015-08-27 12:57:56 +00:00
g_Config.m_BrDemoSortOrder ^= 1;
2015-08-25 13:55:15 +00:00
else
2015-08-27 12:57:56 +00:00
g_Config.m_BrDemoSortOrder = 0;
g_Config.m_BrDemoSort = Col.m_Sort;
2015-08-25 13:55:15 +00:00
}
2015-08-27 12:57:56 +00:00
// Don't rescan in order to keep fetched headers, just resort
2023-08-28 19:11:12 +00:00
std::stable_sort(m_vDemos.begin(), m_vDemos.end());
2015-08-27 12:57:56 +00:00
DemolistOnUpdate(false);
}
}
if(m_DemolistSelectedReveal)
{
s_ListBox.ScrollToSelected();
m_DemolistSelectedReveal = false;
}
2023-08-25 23:55:30 +00:00
2023-08-26 11:13:12 +00:00
s_ListBox.DoStart(ms_ListheaderHeight, m_vpFilteredDemos.size(), 1, 3, m_DemolistSelectedIndex, &ListBox, false, IGraphics::CORNER_ALL, true);
2015-08-27 12:57:56 +00:00
2015-08-25 13:55:15 +00:00
int ItemIndex = -1;
2023-08-26 11:13:12 +00:00
for(auto &Item : m_vpFilteredDemos)
2015-08-25 13:55:15 +00:00
{
ItemIndex++;
const CListboxItem ListItem = s_ListBox.DoNextItem(&Item, ItemIndex == m_DemolistSelectedIndex);
if(!ListItem.m_Visible)
2015-08-25 13:55:15 +00:00
continue;
for(const auto &Col : s_aCols)
2015-08-25 13:55:15 +00:00
{
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;
2015-08-25 13:55:15 +00:00
int ID = Col.m_ID;
2015-08-25 13:55:15 +00:00
if(ID == COL_ICON)
2015-08-25 13:55:15 +00:00
{
Button.Margin(1.0f, &Button);
const char *pIconType;
2023-08-26 11:13:12 +00:00
if(Item->m_IsLink || str_comp(Item->m_aFilename, "..") == 0)
pIconType = FONT_ICON_FOLDER_TREE;
2023-08-26 11:13:12 +00:00
else if(Item->m_IsDir)
pIconType = FONT_ICON_FOLDER;
else
pIconType = FONT_ICON_FILM;
ColorRGBA IconColor;
2023-08-26 11:13:12 +00:00
if(!Item->m_IsDir && (!Item->m_InfosLoaded || !Item->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);
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
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());
Add font index, support font family variants depending on language Add `fonts/index.json` which specifies: - List of all font files that should be loaded (filenames). - Default font (specified by family name or by family and style name). - Font variants for different languages, using the name of the language file as key. - Fallback fonts. - Icon font. There are characters (e.g. all in `刃直海角骨入`) that look different depending on the language of the content being Japanese, Simplified Chinese, Traditional Chinese and Hangul, because Unicode uses the same codepoint for characters regardless of the language. To render these characters correctly, the active variant font is switched depending on the selected language. The `ITextRender` interface is changed so the current language variant can be set using `SetFontLanguageVariant` and the default and icon fonts can be toggled using `SetFontPreset`. The class `CFont` is removed entirely. The text render is restructured: The font faces and font atlas are now managed by a separate class `CGlyphMap` like on upstream. As the text fill and outline textures always have the same size, the texture skyline only needs to be stored once and free positions in the atlas only need to be calculated once for each glyph instead of separately for the fill and outline textures. The font files and their licenses are also updated: - Update Source Han Sans to version 2.001. - Update Glow Sans Japanese Compressed to version 0.93. - Update Deja Vu Sans to version 2.37. - Update Font Awesome icons font to March 2023 version. Closes #6881.
2023-07-17 13:49:41 +00:00
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
}
else if(ID == COL_DEMONAME)
{
SLabelProperties Props;
Props.m_MaxWidth = Button.w;
Props.m_EllipsisAtEnd = true;
Props.m_EnableWidthCheck = false;
2023-08-26 11:13:12 +00:00
UI()->DoLabel(&Button, Item->m_aName, 12.0f, TEXTALIGN_ML, Props);
2015-08-25 13:55:15 +00:00
}
2023-08-26 11:13:12 +00:00
else if(ID == COL_MARKERS && !Item->m_IsDir && Item->m_InfosLoaded && Item->m_Valid)
{
char aBuf[3];
2023-08-26 11:13:12 +00:00
str_from_int(Item->NumMarkers(), aBuf);
Button.VMargin(4.0f, &Button);
UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR);
}
2023-08-26 11:13:12 +00:00
else if(ID == COL_LENGTH && !Item->m_IsDir && Item->m_InfosLoaded && Item->m_Valid)
{
char aBuf[32];
2023-08-26 11:13:12 +00:00
str_time((int64_t)Item->Length() * 100, TIME_HOURS, aBuf, sizeof(aBuf));
Button.VMargin(4.0f, &Button);
UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR);
}
2023-08-26 11:13:12 +00:00
else if(ID == COL_DATE && !Item->m_IsDir)
2015-08-25 13:55:15 +00:00
{
char aBuf[64];
2023-08-26 11:13:12 +00:00
str_timestamp_ex(Item->m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE);
Button.VMargin(4.0f, &Button);
UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR);
2015-08-25 13:55:15 +00:00
}
}
}
const int NewSelected = s_ListBox.DoEnd();
if(NewSelected != m_DemolistSelectedIndex)
2015-08-27 12:57:56 +00:00
{
m_DemolistSelectedIndex = NewSelected;
if(m_DemolistSelectedIndex >= 0)
2023-08-26 11:13:12 +00:00
str_copy(m_aCurrentDemoSelectionName, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
DemolistOnUpdate(false);
2015-08-27 12:57:56 +00:00
}
2022-07-16 13:32:06 +00:00
static CButtonContainer s_RefreshButton;
if(DoButton_Menu(&s_RefreshButton, Localize("Refresh"), 0, &RefreshRect) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed()))
{
2010-05-29 07:25:38 +00:00
DemolistPopulate();
DemolistOnUpdate(false);
2010-05-29 07:25:38 +00:00
}
if(DoButton_CheckBox(&g_Config.m_BrDemoFetchInfo, Localize("Fetch Info"), g_Config.m_BrDemoFetchInfo, &FetchRect))
{
g_Config.m_BrDemoFetchInfo ^= 1;
if(g_Config.m_BrDemoFetchInfo)
FetchAllHeaders();
}
2022-07-16 13:32:06 +00:00
static CButtonContainer s_PlayButton;
2023-08-26 11:13:12 +00:00
if(DoButton_Menu(&s_PlayButton, (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? Localize("Open") : Localize("Play", "Demo browser"), 0, &PlayRect) || s_ListBox.WasItemActivated() || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
2010-09-03 19:41:37 +00:00
{
if(m_DemolistSelectedIndex >= 0)
{
2023-08-26 11:13:12 +00:00
if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder
2010-06-04 20:14:02 +00:00
{
2023-08-25 23:55:30 +00:00
m_DemoSearchInput.Clear();
2023-08-26 11:13:12 +00:00
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
2023-08-26 11:13:12 +00:00
m_DemolistStorageType = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType;
str_append(m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
}
2010-06-04 20:14:02 +00:00
DemolistPopulate();
DemolistOnUpdate(!ParentFolder);
2010-06-04 20:14:02 +00:00
}
else // file
2010-06-04 20:14:02 +00:00
{
char aBuf[IO_MAX_PATH_LENGTH];
2023-08-26 11:13:12 +00:00
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;
2010-06-04 20:14:02 +00:00
if(pError)
2010-11-20 22:46:49 +00:00
PopupMessage(Localize("Error"), str_comp(pError, "error loading demo") ? pError : Localize("Error loading demo"), Localize("Ok"));
else
{
UI()->SetActiveItem(nullptr);
return;
}
2010-06-04 20:14:02 +00:00
}
}
}
2022-07-16 13:32:06 +00:00
static CButtonContainer s_DirectoryButtonID;
2021-05-17 07:11:36 +00:00
if(DoButton_Menu(&s_DirectoryButtonID, Localize("Demos directory"), 0, &DirectoryButton))
2020-09-29 01:23:39 +00:00
{
char aBuf[IO_MAX_PATH_LENGTH];
2023-08-26 11:13:12 +00:00
Storage()->GetCompletePath(m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, aBuf, sizeof(aBuf));
2021-12-19 00:13:08 +00:00
if(!open_file(aBuf))
2020-09-29 01:23:39 +00:00
{
dbg_msg("menus", "couldn't open file '%s'", aBuf);
2020-09-29 01:23:39 +00:00
}
}
GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButtonID, &DirectoryButton, Localize("Open the directory that contains the demo files"));
2020-09-29 01:23:39 +00:00
if(m_DemolistSelectedIndex >= 0 && m_aCurrentDemoFolder[0] != '\0')
{
2023-08-26 11:13:12 +00:00
if(str_comp(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, "..") != 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE)
{
static CButtonContainer s_DeleteButton;
2023-08-25 23:55:30 +00:00
if(DoButton_Menu(&s_DeleteButton, Localize("Delete"), 0, &DeleteRect) || UI()->ConsumeHotkey(CUI::HOTKEY_DELETE) || (Input()->KeyPress(KEY_D) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
{
char aBuf[128 + IO_MAX_PATH_LENGTH];
2023-08-26 11:13:12 +00:00
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;
}
static CButtonContainer s_RenameButton;
if(DoButton_Menu(&s_RenameButton, Localize("Rename"), 0, &RenameRect))
{
m_Popup = POPUP_RENAME_DEMO;
2023-08-26 11:13:12 +00:00
if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
{
2023-08-26 11:13:12 +00:00
m_DemoRenameInput.Set(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
}
else
{
char aNameWithoutExt[IO_MAX_PATH_LENGTH];
2023-08-26 11:13:12 +00:00
fs_split_file_extension(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, aNameWithoutExt, sizeof(aNameWithoutExt));
m_DemoRenameInput.Set(aNameWithoutExt);
}
UI()->SetActiveItem(&m_DemoRenameInput);
return;
}
}
2019-09-26 15:28:29 +00:00
2019-09-27 07:22:50 +00:00
#if defined(CONF_VIDEORECORDER)
2023-08-26 11:13:12 +00:00
if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
2019-09-26 15:28:29 +00:00
{
static CButtonContainer s_RenderButton;
2023-08-25 23:55:30 +00:00
if(DoButton_Menu(&s_RenderButton, Localize("Render"), 0, &RenderRect) || (Input()->KeyPress(KEY_R) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive()))
{
m_Popup = POPUP_RENDER_DEMO;
m_StartPaused = false;
char aNameWithoutExt[IO_MAX_PATH_LENGTH];
2023-08-26 11:13:12 +00:00
fs_split_file_extension(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, aNameWithoutExt, sizeof(aNameWithoutExt));
m_DemoRenderInput.Set(aNameWithoutExt);
UI()->SetActiveItem(&m_DemoRenderInput);
return;
}
2019-09-26 15:28:29 +00:00
}
2019-09-27 07:22:50 +00:00
#endif
}
UI()->DoLabel(&LabelRect, aFooterLabel, 14.0f, TEXTALIGN_ML);
2023-08-25 23:55:30 +00:00
// render quick search
{
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);
UI()->DoLabel(&DemoSearch, FONT_ICON_MAGNIFYING_GLASS, 16.0f, TEXTALIGN_ML);
TextRender()->SetRenderFlags(0);
TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
2023-08-26 11:13:12 +00:00
DemoSearch.VSplitLeft(TextRender()->TextWidth(16.0f, FONT_ICON_MAGNIFYING_GLASS), nullptr, &DemoSearch);
2023-08-25 23:55:30 +00:00
DemoSearch.VSplitLeft(5.0f, 0, &DemoSearch);
m_DemoSearchInput.SetEmptyText(Localize("Search"));
if(Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed())
{
UI()->SetActiveItem(&m_DemoSearchInput);
m_DemoSearchInput.SelectAll();
}
2023-08-26 11:13:12 +00:00
if(UI()->DoClearableEditBox(&m_DemoSearchInput, &DemoSearch, 12.0f))
{
RefreshFilteredDemos();
DemolistOnUpdate(false);
}
2023-08-25 23:55:30 +00:00
}
}
void CMenus::PopupConfirmDeleteDemo()
{
char aBuf[IO_MAX_PATH_LENGTH];
2023-08-26 11:13:12 +00:00
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];
2023-08-26 11:13:12 +00:00
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];
2023-08-26 11:13:12 +00:00
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];
2023-08-26 11:13:12 +00:00
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"));
}
}