1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "maplayers.h"
5#include "menus.h"
6
7#include <base/hash.h>
8#include <base/math.h>
9#include <base/system.h>
10
11#include <engine/client.h>
12#include <engine/demo.h>
13#include <engine/graphics.h>
14#include <engine/keys.h>
15#include <engine/shared/localization.h>
16#include <engine/storage.h>
17#include <engine/textrender.h>
18
19#include <generated/client_data.h>
20
21#include <game/client/components/console.h>
22#include <game/client/gameclient.h>
23#include <game/client/ui.h>
24#include <game/client/ui_listbox.h>
25#include <game/localization.h>
26
27#include <chrono>
28
29using namespace FontIcons;
30using namespace std::chrono_literals;
31
32bool CMenus::DemoFilterChat(const void *pData, int Size, void *pUser)
33{
34 bool DoFilterChat = *(bool *)pUser;
35 if(!DoFilterChat)
36 {
37 return false;
38 }
39
40 CUnpacker Unpacker;
41 Unpacker.Reset(pData, Size);
42
43 int Msg = Unpacker.GetInt();
44 int Sys = Msg & 1;
45 Msg >>= 1;
46
47 return !Unpacker.Error() && !Sys && Msg == NETMSGTYPE_SV_CHAT;
48}
49
50void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek)
51{
52 if((PositionToSeek >= 0.0f && PositionToSeek <= 1.0f) || TimeToSeek != 0.0f)
53 {
54 GameClient()->m_Chat.Reset();
55 GameClient()->m_DamageInd.OnReset();
56 GameClient()->m_InfoMessages.OnReset();
57 GameClient()->m_Particles.OnReset();
58 GameClient()->m_Sounds.OnReset();
59 GameClient()->m_Scoreboard.OnReset();
60 GameClient()->m_Statboard.OnReset();
61 GameClient()->m_SuppressEvents = true;
62 if(TimeToSeek != 0.0f)
63 DemoPlayer()->SeekTime(Seconds: TimeToSeek);
64 else
65 DemoPlayer()->SeekPercent(Percent: PositionToSeek);
66 GameClient()->m_SuppressEvents = false;
67
68 if(!DemoPlayer()->BaseInfo()->m_Paused &&
69 !DemoPlayer()->BaseInfo()->m_LiveDemo &&
70 PositionToSeek == 1.0f)
71 {
72 DemoPlayer()->Pause();
73 }
74 }
75}
76
77void CMenus::DemoSeekTick(IDemoPlayer::ETickOffset TickOffset)
78{
79 GameClient()->m_SuppressEvents = true;
80 DemoPlayer()->SeekTick(TickOffset);
81 GameClient()->m_SuppressEvents = false;
82 DemoPlayer()->Pause();
83}
84
85void CMenus::RenderDemoPlayer(CUIRect MainView)
86{
87 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
88 const int CurrentTick = pInfo->m_CurrentTick - pInfo->m_FirstTick;
89 const int TotalTicks = pInfo->m_LastTick - pInfo->m_FirstTick;
90
91 // When rendering a demo and starting paused, render the pause indicator permanently.
92#if defined(CONF_VIDEORECORDER)
93 const bool VideoRendering = IVideo::Current() != nullptr;
94 bool InitialVideoPause = VideoRendering && m_LastPauseChange < 0.0f && pInfo->m_Paused;
95#else
96 const bool VideoRendering = false;
97 bool InitialVideoPause = false;
98#endif
99
100 const auto &&UpdateLastPauseChange = [&]() {
101 // Immediately hide the pause indicator when unpausing the initial pause when rendering a demo.
102 m_LastPauseChange = InitialVideoPause ? 0.0f : Client()->GlobalTime();
103 InitialVideoPause = false;
104 };
105 const auto &&UpdateLastSpeedChange = [&]() {
106 m_LastSpeedChange = Client()->GlobalTime();
107 };
108
109 // threshold value, accounts for slight inaccuracy when setting demo position
110 constexpr int Threshold = 10;
111 const auto &&FindPreviousMarkerPosition = [&]() {
112 for(int i = pInfo->m_NumTimelineMarkers - 1; i >= 0; i--)
113 {
114 if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) < CurrentTick && absolute(a: ((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
115 {
116 return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
117 }
118 }
119 return 0.0f;
120 };
121 const auto &&FindNextMarkerPosition = [&]() {
122 for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
123 {
124 if((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) > CurrentTick && absolute(a: ((pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) - CurrentTick)) > Threshold)
125 {
126 return (float)(pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / TotalTicks;
127 }
128 }
129 return 1.0f;
130 };
131
132 static constexpr float SKIP_DURATIONS_SECONDS[] = {0.1f, 0.5f, 1.0f, 5.0f, 10.0f, 30.0f, 60.0f, 5.0f * 60.0f, 10.0f * 60.0f};
133 static constexpr const char *SKIP_DURATIONS_STRINGS[] = {"0.1", "0.5", "1", "5", "10", "30", "1", "5", "10"};
134 static_assert(SKIP_DURATIONS_SECONDS[DEFAULT_SKIP_DURATION_INDEX] == 5.0f);
135 static_assert(std::size(SKIP_DURATIONS_SECONDS) == std::size(SKIP_DURATIONS_STRINGS));
136
137 const int DemoLengthSeconds = TotalTicks / Client()->GameTickSpeed();
138 int NumDurationLabels = 0;
139 for(size_t i = 0; i < std::size(SKIP_DURATIONS_SECONDS); ++i)
140 {
141 if(SKIP_DURATIONS_SECONDS[i] >= DemoLengthSeconds)
142 break;
143 NumDurationLabels = i + 1;
144 }
145 if(NumDurationLabels > 0 && m_SkipDurationIndex >= NumDurationLabels)
146 m_SkipDurationIndex = maximum(a: 0, b: NumDurationLabels - 1);
147
148 // handle keyboard shortcuts independent of active menu
149 float PositionToSeek = -1.0f;
150 float TimeToSeek = 0.0f;
151 if(!GameClient()->m_GameConsole.IsActive() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts && !Ui()->IsPopupOpen())
152 {
153 // increase/decrease speed
154 if(!Input()->ModifierIsPressed() && !Input()->ShiftIsPressed() && !Input()->AltIsPressed())
155 {
156 if(Input()->KeyPress(Key: KEY_UP) || (m_MenuActive && Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP)))
157 {
158 DemoPlayer()->AdjustSpeedIndex(Offset: +1);
159 UpdateLastSpeedChange();
160 }
161 else if(Input()->KeyPress(Key: KEY_DOWN) || (m_MenuActive && Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN)))
162 {
163 DemoPlayer()->AdjustSpeedIndex(Offset: -1);
164 UpdateLastSpeedChange();
165 }
166 }
167
168 // pause/unpause
169 if(Input()->KeyPress(Key: KEY_SPACE) || Input()->KeyPress(Key: KEY_RETURN) || Input()->KeyPress(Key: KEY_KP_ENTER) || Input()->KeyPress(Key: KEY_K))
170 {
171 if(pInfo->m_Paused)
172 {
173 DemoPlayer()->Unpause();
174 }
175 else
176 {
177 DemoPlayer()->Pause();
178 }
179 UpdateLastPauseChange();
180 }
181
182 // seek backward/forward configured time
183 if(Input()->KeyPress(Key: KEY_LEFT) || Input()->KeyPress(Key: KEY_J))
184 {
185 if(Input()->ModifierIsPressed())
186 PositionToSeek = FindPreviousMarkerPosition();
187 else if(Input()->ShiftIsPressed())
188 m_SkipDurationIndex = maximum(a: m_SkipDurationIndex - 1, b: 0);
189 else
190 TimeToSeek = -SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
191 }
192 else if(Input()->KeyPress(Key: KEY_RIGHT) || Input()->KeyPress(Key: KEY_L))
193 {
194 if(Input()->ModifierIsPressed())
195 PositionToSeek = FindNextMarkerPosition();
196 else if(Input()->ShiftIsPressed())
197 m_SkipDurationIndex = minimum(a: m_SkipDurationIndex + 1, b: NumDurationLabels - 1);
198 else
199 TimeToSeek = SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
200 }
201
202 // seek to 0-90%
203 const int aSeekPercentKeys[] = {KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9};
204 for(unsigned i = 0; i < std::size(aSeekPercentKeys); i++)
205 {
206 if(Input()->KeyPress(Key: aSeekPercentKeys[i]))
207 {
208 PositionToSeek = i * 0.1f;
209 break;
210 }
211 }
212
213 // seek to the beginning/end
214 if(Input()->KeyPress(Key: KEY_HOME))
215 {
216 PositionToSeek = 0.0f;
217 }
218 else if(Input()->KeyPress(Key: KEY_END))
219 {
220 PositionToSeek = 1.0f;
221 }
222
223 // Advance single frame forward/backward with period/comma key
224 if(Input()->KeyPress(Key: KEY_PERIOD))
225 {
226 DemoSeekTick(TickOffset: IDemoPlayer::TICK_NEXT);
227 }
228 else if(Input()->KeyPress(Key: KEY_COMMA))
229 {
230 DemoSeekTick(TickOffset: IDemoPlayer::TICK_PREVIOUS);
231 }
232 }
233
234 const float SeekBarHeight = 15.0f;
235 const float ButtonbarHeight = 20.0f;
236 const float NameBarHeight = 20.0f;
237 const float Margins = 5.0f;
238 const float TotalHeight = SeekBarHeight + ButtonbarHeight + NameBarHeight + Margins * 3;
239
240 if(!m_MenuActive)
241 {
242 // Render pause indicator
243 if(g_Config.m_ClDemoShowPause && (InitialVideoPause || (!VideoRendering && Client()->GlobalTime() - m_LastPauseChange < 0.5f)))
244 {
245 const float Time = InitialVideoPause ? 0.5f : ((Client()->GlobalTime() - m_LastPauseChange) / 0.5f);
246 const float Alpha = (Time < 0.5f ? Time : (1.0f - Time)) * 2.0f;
247 if(Alpha > 0.0f)
248 {
249 TextRender()->TextColor(Color: TextRender()->DefaultTextColor().WithMultipliedAlpha(alpha: Alpha));
250 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(alpha: Alpha));
251 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
252 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
253 Ui()->DoLabel(pRect: Ui()->Screen(), pText: pInfo->m_Paused ? FONT_ICON_PAUSE : FONT_ICON_PLAY, Size: 36.0f + Time * 12.0f, Align: TEXTALIGN_MC);
254 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
255 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
256 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
257 TextRender()->SetRenderFlags(0);
258 }
259 }
260
261 // Render speed info
262 if(g_Config.m_ClDemoShowSpeed && Client()->GlobalTime() - m_LastSpeedChange < 1.0f)
263 {
264 CUIRect Screen = *Ui()->Screen();
265
266 char aSpeedBuf[16];
267 str_format(buffer: aSpeedBuf, buffer_size: sizeof(aSpeedBuf), format: "×%.2f", pInfo->m_Speed);
268 TextRender()->Text(x: 120.0f, y: Screen.y + Screen.h - 120.0f - TotalHeight, Size: 60.0f, pText: aSpeedBuf, LineWidth: -1.0f);
269 }
270 }
271 else
272 {
273 if(m_LastPauseChange > 0.0f)
274 m_LastPauseChange = 0.0f;
275 if(m_LastSpeedChange > 0.0f)
276 m_LastSpeedChange = 0.0f;
277 }
278
279 if(CurrentTick == TotalTicks)
280 {
281 DemoPlayer()->Pause();
282 PositionToSeek = 0.0f;
283 UpdateLastPauseChange();
284 }
285
286 if(!m_MenuActive)
287 {
288 HandleDemoSeeking(PositionToSeek, TimeToSeek);
289 return;
290 }
291
292 CUIRect DemoControls;
293 MainView.HSplitBottom(Cut: TotalHeight, pTop: nullptr, pBottom: &DemoControls);
294 DemoControls.VSplitLeft(Cut: 50.0f, pLeft: nullptr, pRight: &DemoControls);
295 DemoControls.VSplitLeft(Cut: 600.0f, pLeft: &DemoControls, pRight: nullptr);
296 const CUIRect DemoControlsOriginal = DemoControls;
297 DemoControls.x += m_DemoControlsPositionOffset.x;
298 DemoControls.y += m_DemoControlsPositionOffset.y;
299 int Corners = IGraphics::CORNER_NONE;
300 if(DemoControls.x > 0.0f && DemoControls.y > 0.0f)
301 Corners |= IGraphics::CORNER_TL;
302 if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y > 0.0f)
303 Corners |= IGraphics::CORNER_TR;
304 if(DemoControls.x > 0.0f && DemoControls.y < MainView.h - DemoControls.h)
305 Corners |= IGraphics::CORNER_BL;
306 if(DemoControls.x < MainView.w - DemoControls.w && DemoControls.y < MainView.h - DemoControls.h)
307 Corners |= IGraphics::CORNER_BR;
308 DemoControls.Draw(Color: ms_ColorTabbarActive, Corners, Rounding: 10.0f);
309 const CUIRect DemoControlsDragRect = DemoControls;
310
311 CUIRect SeekBar, ButtonBar, NameBar, SpeedBar;
312 DemoControls.Margin(Cut: 5.0f, pOtherRect: &DemoControls);
313 DemoControls.HSplitTop(Cut: SeekBarHeight, pTop: &SeekBar, pBottom: &ButtonBar);
314 ButtonBar.HSplitTop(Cut: Margins, pTop: nullptr, pBottom: &ButtonBar);
315 ButtonBar.HSplitBottom(Cut: NameBarHeight, pTop: &ButtonBar, pBottom: &NameBar);
316 NameBar.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &NameBar);
317
318 // handle draggable demo controls
319 {
320 enum EDragOperation
321 {
322 OP_NONE,
323 OP_DRAGGING,
324 OP_CLICKED
325 };
326 static EDragOperation s_Operation = OP_NONE;
327 static vec2 s_InitialMouse = vec2(0.0f, 0.0f);
328
329 bool Clicked;
330 bool Abrupted;
331 if(int Result = Ui()->DoDraggableButtonLogic(pId: &s_Operation, Checked: 8, pRect: &DemoControlsDragRect, pClicked: &Clicked, pAbrupted: &Abrupted))
332 {
333 if(s_Operation == OP_NONE && Result == 1)
334 {
335 s_InitialMouse = Ui()->MousePos();
336 s_Operation = OP_CLICKED;
337 }
338
339 if(Clicked || Abrupted)
340 s_Operation = OP_NONE;
341
342 if(s_Operation == OP_CLICKED && length(a: Ui()->MousePos() - s_InitialMouse) > 5.0f)
343 {
344 s_Operation = OP_DRAGGING;
345 s_InitialMouse -= m_DemoControlsPositionOffset;
346 }
347
348 if(s_Operation == OP_DRAGGING)
349 {
350 m_DemoControlsPositionOffset = Ui()->MousePos() - s_InitialMouse;
351 m_DemoControlsPositionOffset.x = std::clamp(val: m_DemoControlsPositionOffset.x, lo: -DemoControlsOriginal.x, hi: MainView.w - DemoControlsDragRect.w - DemoControlsOriginal.x);
352 m_DemoControlsPositionOffset.y = std::clamp(val: m_DemoControlsPositionOffset.y, lo: -DemoControlsOriginal.y, hi: MainView.h - DemoControlsDragRect.h - DemoControlsOriginal.y);
353 }
354 }
355 }
356
357 // Live button
358 if(pInfo->m_LiveDemo)
359 {
360 CUIRect LiveButton;
361 SeekBar.VSplitRight(Cut: SeekBar.h, pLeft: &SeekBar, pRight: &LiveButton);
362 SeekBar.VSplitRight(Cut: 2.0f, pLeft: &SeekBar, pRight: nullptr);
363 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
364 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_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
365 TextRender()->TextColor(Color: pInfo->m_LivePlayback ? ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f) : ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f));
366 Ui()->DoLabel(pRect: &LiveButton, pText: FONT_ICON_CIRCLE, Size: 24.0f, Align: TEXTALIGN_MC);
367 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
368 TextRender()->SetRenderFlags(0);
369 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
370 static char s_LiveButtonId;
371 if(Ui()->HotItem() == &s_LiveButtonId)
372 {
373 LiveButton.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
374 }
375 if(Ui()->DoButtonLogic(pId: &s_LiveButtonId, Checked: 0, pRect: &LiveButton, Flags: BUTTONFLAG_LEFT))
376 {
377 PositionToSeek = 1.0f;
378 DemoPlayer()->SetSpeedIndex(DEMO_SPEED_INDEX_DEFAULT);
379 UpdateLastSpeedChange();
380 }
381 GameClient()->m_Tooltips.DoToolTip(pId: &s_LiveButtonId, pNearRect: &LiveButton,
382 pText: pInfo->m_LivePlayback ? Localize(pStr: "Live", pContext: "Demo playback") : Localize(pStr: "Go to Live", pContext: "Demo playback"));
383 }
384
385 // do seekbar
386 {
387 // draw seek bar
388 const float Rounding = 5.0f;
389 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
390
391 // draw filled bar
392 float Amount = CurrentTick / (float)TotalTicks;
393 CUIRect FilledBar = SeekBar;
394 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
395 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
396
397 // draw highlighting
398 if(g_Config.m_ClDemoSliceBegin != -1 && g_Config.m_ClDemoSliceEnd != -1)
399 {
400 float RatioBegin = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
401 float RatioEnd = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
402 float Span = ((SeekBar.w - 2 * Rounding) * RatioEnd) - ((SeekBar.w - 2 * Rounding) * RatioBegin);
403 Graphics()->TextureClear();
404 Graphics()->QuadsBegin();
405 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 0.25f);
406 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * RatioBegin, SeekBar.y, Span, SeekBar.h);
407 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
408 Graphics()->QuadsEnd();
409 }
410
411 // draw markers
412 for(int i = 0; i < pInfo->m_NumTimelineMarkers; i++)
413 {
414 float Ratio = (pInfo->m_aTimelineMarkers[i] - pInfo->m_FirstTick) / (float)TotalTicks;
415 Graphics()->TextureClear();
416 Graphics()->QuadsBegin();
417 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
418 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
419 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
420 Graphics()->QuadsEnd();
421 }
422
423 // draw slice markers
424 // begin
425 if(g_Config.m_ClDemoSliceBegin != -1)
426 {
427 float Ratio = (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick) / (float)TotalTicks;
428 Graphics()->TextureClear();
429 Graphics()->QuadsBegin();
430 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
431 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
432 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
433 Graphics()->QuadsEnd();
434 }
435
436 // end
437 if(g_Config.m_ClDemoSliceEnd != -1)
438 {
439 float Ratio = (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick) / (float)TotalTicks;
440 Graphics()->TextureClear();
441 Graphics()->QuadsBegin();
442 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
443 IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h);
444 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
445 Graphics()->QuadsEnd();
446 }
447
448 // draw time
449 char aCurrentTime[32];
450 str_time(centisecs: (int64_t)CurrentTick / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
451 char aTotalTime[32];
452 str_time(centisecs: (int64_t)TotalTicks / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
453 char aSeekBarLabel[128];
454 str_format(buffer: aSeekBarLabel, buffer_size: sizeof(aSeekBarLabel), format: "%s / %s", aCurrentTime, aTotalTime);
455 Ui()->DoLabel(pRect: &SeekBar, pText: aSeekBarLabel, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
456
457 // do the logic
458 const bool Inside = Ui()->MouseInside(pRect: &SeekBar);
459
460 static char s_SeekBarId;
461 if(Ui()->CheckActiveItem(pId: &s_SeekBarId))
462 {
463 if(!Ui()->MouseButton(Index: 0))
464 Ui()->SetActiveItem(nullptr);
465 else
466 {
467 static float s_PrevAmount = 0.0f;
468 float AmountSeek = std::clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
469
470 if(Input()->ShiftIsPressed())
471 {
472 AmountSeek = s_PrevAmount + (AmountSeek - s_PrevAmount) * 0.05f;
473 if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(a: s_PrevAmount - AmountSeek) >= 0.0001f)
474 {
475 PositionToSeek = AmountSeek;
476 }
477 }
478 else
479 {
480 if(AmountSeek >= 0.0f && AmountSeek <= 1.0f && absolute(a: s_PrevAmount - AmountSeek) >= 0.001f)
481 {
482 s_PrevAmount = AmountSeek;
483 PositionToSeek = AmountSeek;
484 }
485 }
486 }
487 }
488 else if(Ui()->HotItem() == &s_SeekBarId)
489 {
490 if(Ui()->MouseButton(Index: 0))
491 {
492 Ui()->SetActiveItem(&s_SeekBarId);
493 }
494 }
495
496 if(Inside && !Ui()->MouseButton(Index: 0))
497 Ui()->SetHotItem(&s_SeekBarId);
498
499 if(Ui()->HotItem() == &s_SeekBarId)
500 {
501 const int HoveredTick = (int)(std::clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f) * TotalTicks);
502 static char s_aHoveredTime[32];
503 str_time(centisecs: (int64_t)HoveredTick / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: s_aHoveredTime, buffer_size: sizeof(s_aHoveredTime));
504 GameClient()->m_Tooltips.DoToolTip(pId: &s_SeekBarId, pNearRect: &SeekBar, pText: s_aHoveredTime);
505 }
506 }
507
508 bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false;
509
510 // do buttons
511 CUIRect Button;
512
513 // combined play and pause button
514 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
515 static CButtonContainer s_PlayPauseButton;
516 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_PlayPauseButton, pText: pInfo->m_Paused ? FONT_ICON_PLAY : FONT_ICON_PAUSE, Checked: false, pRect: &Button, Flags: BUTTONFLAG_LEFT))
517 {
518 if(pInfo->m_Paused)
519 {
520 DemoPlayer()->Unpause();
521 }
522 else
523 {
524 DemoPlayer()->Pause();
525 }
526 UpdateLastPauseChange();
527 }
528 GameClient()->m_Tooltips.DoToolTip(pId: &s_PlayPauseButton, pNearRect: &Button, pText: pInfo->m_Paused ? Localize(pStr: "Play the current demo") : Localize(pStr: "Pause the current demo"));
529
530 // stop button
531 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
532 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
533 static CButtonContainer s_ResetButton;
534 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ResetButton, pText: FONT_ICON_STOP, Checked: false, pRect: &Button, Flags: BUTTONFLAG_LEFT))
535 {
536 DemoPlayer()->Pause();
537 PositionToSeek = 0.0f;
538 }
539 GameClient()->m_Tooltips.DoToolTip(pId: &s_ResetButton, pNearRect: &Button, pText: Localize(pStr: "Stop the current demo"));
540
541 // skip time back
542 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
543 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
544 static CButtonContainer s_TimeBackButton;
545 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_TimeBackButton, pText: FONT_ICON_BACKWARD, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
546 {
547 TimeToSeek = -SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
548 }
549 GameClient()->m_Tooltips.DoToolTip(pId: &s_TimeBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back the specified duration"));
550
551 // skip time dropdown
552 if(NumDurationLabels >= 2)
553 {
554 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
555 ButtonBar.VSplitLeft(Cut: 4 * ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
556
557 static std::vector<std::string> s_vDurationNames;
558 static std::vector<const char *> s_vpDurationNames;
559 s_vDurationNames.resize(new_size: NumDurationLabels);
560 s_vpDurationNames.resize(new_size: NumDurationLabels);
561
562 for(int i = 0; i < NumDurationLabels; ++i)
563 {
564 char aBuf[256];
565 if(SKIP_DURATIONS_SECONDS[i] >= 60)
566 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%s min.", pContext: "Demo player duration"), SKIP_DURATIONS_STRINGS[i]);
567 else
568 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%s sec.", pContext: "Demo player duration"), SKIP_DURATIONS_STRINGS[i]);
569 s_vDurationNames[i] = aBuf;
570 s_vpDurationNames[i] = s_vDurationNames[i].c_str();
571 }
572
573 static CUi::SDropDownState s_SkipDurationDropDownState;
574 static CScrollRegion s_SkipDurationDropDownScrollRegion;
575 s_SkipDurationDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_SkipDurationDropDownScrollRegion;
576 m_SkipDurationIndex = Ui()->DoDropDown(pRect: &Button, CurSelection: m_SkipDurationIndex, pStrs: s_vpDurationNames.data(), Num: NumDurationLabels, State&: s_SkipDurationDropDownState);
577 GameClient()->m_Tooltips.DoToolTip(pId: &s_SkipDurationDropDownState.m_ButtonContainer, pNearRect: &Button, pText: Localize(pStr: "Change the skip duration"));
578 }
579
580 // skip time forward
581 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
582 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
583 static CButtonContainer s_TimeForwardButton;
584 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_TimeForwardButton, pText: FONT_ICON_FORWARD, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
585 {
586 TimeToSeek = SKIP_DURATIONS_SECONDS[m_SkipDurationIndex];
587 }
588 GameClient()->m_Tooltips.DoToolTip(pId: &s_TimeForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward the specified duration"));
589
590 // one tick back
591 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
592 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
593 static CButtonContainer s_OneTickBackButton;
594 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneTickBackButton, pText: FONT_ICON_BACKWARD_STEP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
595 {
596 DemoSeekTick(TickOffset: IDemoPlayer::TICK_PREVIOUS);
597 }
598 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneTickBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back one tick"));
599
600 // one tick forward
601 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
602 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
603 static CButtonContainer s_OneTickForwardButton;
604 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneTickForwardButton, pText: FONT_ICON_FORWARD_STEP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
605 {
606 DemoSeekTick(TickOffset: IDemoPlayer::TICK_NEXT);
607 }
608 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneTickForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward one tick"));
609
610 // one marker back
611 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
612 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
613 static CButtonContainer s_OneMarkerBackButton;
614 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneMarkerBackButton, pText: FONT_ICON_BACKWARD_FAST, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
615 {
616 PositionToSeek = FindPreviousMarkerPosition();
617 }
618 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneMarkerBackButton, pNearRect: &Button, pText: Localize(pStr: "Go back one marker"));
619
620 // one marker forward
621 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
622 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
623 static CButtonContainer s_OneMarkerForwardButton;
624 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_OneMarkerForwardButton, pText: FONT_ICON_FORWARD_FAST, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
625 {
626 PositionToSeek = FindNextMarkerPosition();
627 }
628 GameClient()->m_Tooltips.DoToolTip(pId: &s_OneMarkerForwardButton, pNearRect: &Button, pText: Localize(pStr: "Go forward one marker"));
629
630 // slowdown
631 ButtonBar.VSplitLeft(Cut: Margins + 10.0f, pLeft: nullptr, pRight: &ButtonBar);
632 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
633 static CButtonContainer s_SlowDownButton;
634 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SlowDownButton, pText: FONT_ICON_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
635 DecreaseDemoSpeed = true;
636 GameClient()->m_Tooltips.DoToolTip(pId: &s_SlowDownButton, pNearRect: &Button, pText: Localize(pStr: "Slow down the demo"));
637
638 // fastforward
639 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
640 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
641 static CButtonContainer s_SpeedUpButton;
642 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SpeedUpButton, pText: FONT_ICON_CHEVRON_UP, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT))
643 IncreaseDemoSpeed = true;
644 GameClient()->m_Tooltips.DoToolTip(pId: &s_SpeedUpButton, pNearRect: &Button, pText: Localize(pStr: "Speed up the demo"));
645
646 // speed meter
647 ButtonBar.VSplitLeft(Cut: Margins * 12, pLeft: &SpeedBar, pRight: &ButtonBar);
648 char aBuffer[64];
649 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "×%g", pInfo->m_Speed);
650 Ui()->DoLabel(pRect: &SpeedBar, pText: aBuffer, Size: Button.h * 0.7f, Align: TEXTALIGN_MC);
651
652 // slice begin button
653 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
654 static CButtonContainer s_SliceBeginButton;
655 const int SliceBeginButtonResult = Ui()->DoButton_FontIcon(pButtonContainer: &s_SliceBeginButton, pText: FONT_ICON_RIGHT_FROM_BRACKET, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
656 if(SliceBeginButtonResult == 1)
657 {
658 Client()->DemoSliceBegin();
659 if(CurrentTick > (g_Config.m_ClDemoSliceEnd - pInfo->m_FirstTick))
660 g_Config.m_ClDemoSliceEnd = -1;
661 }
662 else if(SliceBeginButtonResult == 2)
663 {
664 g_Config.m_ClDemoSliceBegin = -1;
665 }
666 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceBeginButton, pNearRect: &Button, pText: Localize(pStr: "Mark the beginning of a cut (right click to reset)"));
667
668 // slice end button
669 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
670 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
671 static CButtonContainer s_SliceEndButton;
672 const int SliceEndButtonResult = Ui()->DoButton_FontIcon(pButtonContainer: &s_SliceEndButton, pText: FONT_ICON_RIGHT_TO_BRACKET, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT | BUTTONFLAG_RIGHT);
673 if(SliceEndButtonResult == 1)
674 {
675 Client()->DemoSliceEnd();
676 if(CurrentTick < (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick))
677 g_Config.m_ClDemoSliceBegin = -1;
678 }
679 else if(SliceEndButtonResult == 2)
680 {
681 g_Config.m_ClDemoSliceEnd = -1;
682 }
683 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceEndButton, pNearRect: &Button, pText: Localize(pStr: "Mark the end of a cut (right click to reset)"));
684
685 // slice save button
686#if defined(CONF_VIDEORECORDER)
687 const bool SliceEnabled = IVideo::Current() == nullptr;
688#else
689 const bool SliceEnabled = true;
690#endif
691 ButtonBar.VSplitLeft(Cut: Margins, pLeft: nullptr, pRight: &ButtonBar);
692 ButtonBar.VSplitLeft(Cut: ButtonbarHeight, pLeft: &Button, pRight: &ButtonBar);
693 static CButtonContainer s_SliceSaveButton;
694 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_SliceSaveButton, pText: FONT_ICON_ARROW_UP_RIGHT_FROM_SQUARE, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled: SliceEnabled) && SliceEnabled)
695 {
696 char aDemoName[IO_MAX_PATH_LENGTH];
697 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
698 m_DemoSliceInput.Set(aDemoName);
699 Ui()->SetActiveItem(&m_DemoSliceInput);
700 m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE;
701 }
702 GameClient()->m_Tooltips.DoToolTip(pId: &s_SliceSaveButton, pNearRect: &Button, pText: Localize(pStr: "Export cut as a separate demo"));
703
704 // close button
705 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
706 static CButtonContainer s_ExitButton;
707 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_ExitButton, pText: FONT_ICON_XMARK, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT) || (Input()->KeyPress(Key: KEY_C) && !GameClient()->m_GameConsole.IsActive() && m_DemoPlayerState == DEMOPLAYER_NONE))
708 {
709 Client()->Disconnect();
710 SetMenuPage(PAGE_DEMOS);
711 DemolistOnUpdate(Reset: false);
712 }
713 GameClient()->m_Tooltips.DoToolTip(pId: &s_ExitButton, pNearRect: &Button, pText: Localize(pStr: "Close the demo player"));
714
715 // toggle keyboard shortcuts button
716 ButtonBar.VSplitRight(Cut: Margins, pLeft: &ButtonBar, pRight: nullptr);
717 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
718 static CButtonContainer s_KeyboardShortcutsButton;
719 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_KeyboardShortcutsButton, pText: FONT_ICON_KEYBOARD, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled: g_Config.m_ClDemoKeyboardShortcuts != 0))
720 {
721 g_Config.m_ClDemoKeyboardShortcuts ^= 1;
722 }
723 GameClient()->m_Tooltips.DoToolTip(pId: &s_KeyboardShortcutsButton, pNearRect: &Button, pText: Localize(pStr: "Toggle keyboard shortcuts"));
724
725 // auto camera button (only available when it is possible to use)
726 if(GameClient()->m_Camera.CanUseAutoSpecCamera())
727 {
728 ButtonBar.VSplitRight(Cut: Margins, pLeft: &ButtonBar, pRight: nullptr);
729 ButtonBar.VSplitRight(Cut: ButtonbarHeight, pLeft: &ButtonBar, pRight: &Button);
730 static CButtonContainer s_AutoCameraButton;
731 if(Ui()->DoButton_FontIcon(pButtonContainer: &s_AutoCameraButton, pText: FONT_ICON_CAMERA, Checked: 0, pRect: &Button, Flags: BUTTONFLAG_LEFT, Corners: IGraphics::CORNER_ALL, Enabled: GameClient()->m_Camera.m_AutoSpecCamera))
732 {
733 GameClient()->m_Camera.m_AutoSpecCamera = !GameClient()->m_Camera.m_AutoSpecCamera;
734 }
735 GameClient()->m_Tooltips.DoToolTip(pId: &s_AutoCameraButton, pNearRect: &Button, pText: Localize(pStr: "Toggle auto camera"));
736 }
737
738 // demo name
739 char aDemoName[IO_MAX_PATH_LENGTH];
740 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
741 char aBuf[IO_MAX_PATH_LENGTH + 128];
742 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "Demofile: %s"), aDemoName);
743 SLabelProperties Props;
744 Props.m_MaxWidth = NameBar.w;
745 Props.m_EllipsisAtEnd = true;
746 Props.m_EnableWidthCheck = false;
747 Ui()->DoLabel(pRect: &NameBar, pText: aBuf, Size: Button.h * 0.5f, Align: TEXTALIGN_ML, LabelProps: Props);
748
749 if(IncreaseDemoSpeed)
750 {
751 DemoPlayer()->AdjustSpeedIndex(Offset: +1);
752 UpdateLastSpeedChange();
753 }
754 else if(DecreaseDemoSpeed)
755 {
756 DemoPlayer()->AdjustSpeedIndex(Offset: -1);
757 UpdateLastSpeedChange();
758 }
759
760 HandleDemoSeeking(PositionToSeek, TimeToSeek);
761
762 // render popups
763 if(m_DemoPlayerState != DEMOPLAYER_NONE)
764 {
765 // prevent element under the active popup from being activated
766 Ui()->SetHotItem(nullptr);
767 }
768 if(m_DemoPlayerState == DEMOPLAYER_SLICE_SAVE)
769 {
770 RenderDemoPlayerSliceSavePopup(MainView);
771 }
772}
773
774void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView)
775{
776 const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo();
777
778 CUIRect Box;
779 MainView.Margin(Cut: 150.0f, pOtherRect: &Box);
780
781 // background
782 Box.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding: 15.0f);
783 Box.Margin(Cut: 24.0f, pOtherRect: &Box);
784
785 // title
786 CUIRect Title;
787 Box.HSplitTop(Cut: 24.0f, pTop: &Title, pBottom: &Box);
788 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
789 Ui()->DoLabel(pRect: &Title, pText: Localize(pStr: "Export demo cut"), Size: 24.0f, Align: TEXTALIGN_MC);
790
791 // slice times
792 CUIRect SliceTimesBar, SliceInterval, SliceLength;
793 Box.HSplitTop(Cut: 24.0f, pTop: &SliceTimesBar, pBottom: &Box);
794 SliceTimesBar.VSplitMid(pLeft: &SliceInterval, pRight: &SliceLength, Spacing: 40.0f);
795 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
796 const int64_t RealSliceBegin = g_Config.m_ClDemoSliceBegin == -1 ? 0 : (g_Config.m_ClDemoSliceBegin - pInfo->m_FirstTick);
797 const int64_t RealSliceEnd = (g_Config.m_ClDemoSliceEnd == -1 ? pInfo->m_LastTick : g_Config.m_ClDemoSliceEnd) - pInfo->m_FirstTick;
798 char aSliceBegin[32];
799 str_time(centisecs: RealSliceBegin / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aSliceBegin, buffer_size: sizeof(aSliceBegin));
800 char aSliceEnd[32];
801 str_time(centisecs: RealSliceEnd / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aSliceEnd, buffer_size: sizeof(aSliceEnd));
802 char aSliceLength[32];
803 str_time(centisecs: (RealSliceEnd - RealSliceBegin) / Client()->GameTickSpeed() * 100, format: TIME_HOURS, buffer: aSliceLength, buffer_size: sizeof(aSliceLength));
804 char aBuf[256];
805 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s – %s", Localize(pStr: "Cut interval"), aSliceBegin, aSliceEnd);
806 Ui()->DoLabel(pRect: &SliceInterval, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_ML);
807 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s: %s", Localize(pStr: "Cut length"), aSliceLength);
808 Ui()->DoLabel(pRect: &SliceLength, pText: aBuf, Size: 18.0f, Align: TEXTALIGN_ML);
809
810 // file name
811 CUIRect NameLabel, NameBox;
812 Box.HSplitTop(Cut: 24.0f, pTop: &NameLabel, pBottom: &Box);
813 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
814 NameLabel.VSplitLeft(Cut: 150.0f, pLeft: &NameLabel, pRight: &NameBox);
815 NameBox.VSplitLeft(Cut: 20.0f, pLeft: nullptr, pRight: &NameBox);
816 Ui()->DoLabel(pRect: &NameLabel, pText: Localize(pStr: "New name:"), Size: 18.0f, Align: TEXTALIGN_ML);
817 Ui()->DoEditBox(pLineInput: &m_DemoSliceInput, pRect: &NameBox, FontSize: 12.0f);
818
819 // remove chat checkbox
820 static int s_RemoveChat = 0;
821
822 CUIRect CheckBoxBar, RemoveChatCheckBox, RenderCutCheckBox;
823 Box.HSplitTop(Cut: 24.0f, pTop: &CheckBoxBar, pBottom: &Box);
824 Box.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &Box);
825 CheckBoxBar.VSplitMid(pLeft: &RemoveChatCheckBox, pRight: &RenderCutCheckBox, Spacing: 40.0f);
826 if(DoButton_CheckBox(pId: &s_RemoveChat, pText: Localize(pStr: "Remove chat"), Checked: s_RemoveChat, pRect: &RemoveChatCheckBox))
827 {
828 s_RemoveChat ^= 1;
829 }
830#if defined(CONF_VIDEORECORDER)
831 static int s_RenderCut = 0;
832 if(DoButton_CheckBox(pId: &s_RenderCut, pText: Localize(pStr: "Render cut to video"), Checked: s_RenderCut, pRect: &RenderCutCheckBox))
833 {
834 s_RenderCut ^= 1;
835 }
836#endif
837
838 // buttons
839 CUIRect ButtonBar, AbortButton, OkButton;
840 Box.HSplitBottom(Cut: 24.0f, pTop: &Box, pBottom: &ButtonBar);
841 ButtonBar.VSplitMid(pLeft: &AbortButton, pRight: &OkButton, Spacing: 40.0f);
842
843 static CButtonContainer s_ButtonAbort;
844 if(DoButton_Menu(pButtonContainer: &s_ButtonAbort, pText: Localize(pStr: "Abort"), Checked: 0, pRect: &AbortButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
845 m_DemoPlayerState = DEMOPLAYER_NONE;
846
847 static CUi::SConfirmPopupContext s_ConfirmPopupContext;
848 static CButtonContainer s_ButtonOk;
849 if(DoButton_Menu(pButtonContainer: &s_ButtonOk, pText: Localize(pStr: "Ok"), Checked: 0, pRect: &OkButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
850 {
851 if(str_endswith(str: m_DemoSliceInput.GetString(), suffix: ".demo"))
852 {
853 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
854 fs_split_file_extension(filename: m_DemoSliceInput.GetString(), name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
855 m_DemoSliceInput.Set(aNameWithoutExt);
856 }
857
858 static CUi::SMessagePopupContext s_MessagePopupContext;
859 char aDemoName[IO_MAX_PATH_LENGTH];
860 DemoPlayer()->GetDemoName(pBuffer: aDemoName, BufferSize: sizeof(aDemoName));
861 if(str_comp(a: aDemoName, b: m_DemoSliceInput.GetString()) == 0)
862 {
863 s_MessagePopupContext.ErrorColor();
864 str_copy(dst&: s_MessagePopupContext.m_aMessage, src: Localize(pStr: "Please use a different filename"));
865 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_MessagePopupContext);
866 }
867 else if(!str_valid_filename(str: m_DemoSliceInput.GetString()))
868 {
869 s_MessagePopupContext.ErrorColor();
870 str_copy(dst&: s_MessagePopupContext.m_aMessage, src: Localize(pStr: "This name cannot be used for files and folders"));
871 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_MessagePopupContext);
872 }
873 else
874 {
875 char aPath[IO_MAX_PATH_LENGTH];
876 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
877 if(Storage()->FileExists(pFilename: aPath, Type: IStorage::TYPE_SAVE))
878 {
879 s_ConfirmPopupContext.Reset();
880 s_ConfirmPopupContext.YesNoButtons();
881 str_copy(dst&: s_ConfirmPopupContext.m_aMessage, src: Localize(pStr: "File already exists, do you want to overwrite it?"));
882 Ui()->ShowPopupConfirm(X: Ui()->MouseX(), Y: OkButton.y + OkButton.h + 5.0f, pContext: &s_ConfirmPopupContext);
883 }
884 else
885 s_ConfirmPopupContext.m_Result = CUi::SConfirmPopupContext::CONFIRMED;
886 }
887 }
888
889 if(s_ConfirmPopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED)
890 {
891 char aPath[IO_MAX_PATH_LENGTH];
892 str_format(buffer: aPath, buffer_size: sizeof(aPath), format: "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString());
893 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_DemoSliceInput.GetString());
894 if(str_endswith(str: m_aCurrentDemoSelectionName, suffix: ".demo"))
895 m_aCurrentDemoSelectionName[str_length(str: m_aCurrentDemoSelectionName) - str_length(str: ".demo")] = '\0';
896
897 Client()->DemoSlice(pDstPath: aPath, pfnFilter: CMenus::DemoFilterChat, pUser: &s_RemoveChat);
898 DemolistPopulate();
899 DemolistOnUpdate(Reset: false);
900 m_DemoPlayerState = DEMOPLAYER_NONE;
901#if defined(CONF_VIDEORECORDER)
902 if(s_RenderCut)
903 {
904 m_Popup = POPUP_RENDER_DEMO;
905 m_StartPaused = false;
906 m_DemoRenderInput.Set(m_aCurrentDemoSelectionName);
907 Ui()->SetActiveItem(&m_DemoRenderInput);
908 if(m_DemolistStorageType != IStorage::TYPE_ALL && m_DemolistStorageType != IStorage::TYPE_SAVE)
909 m_DemolistStorageType = IStorage::TYPE_ALL; // Select a storage type containing the sliced demo
910 }
911#endif
912 }
913 if(s_ConfirmPopupContext.m_Result != CUi::SConfirmPopupContext::UNSET)
914 {
915 s_ConfirmPopupContext.Reset();
916 }
917}
918
919int CMenus::DemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
920{
921 CMenus *pSelf = (CMenus *)pUser;
922 if(str_comp(a: pInfo->m_pName, b: ".") == 0 ||
923 (str_comp(a: pInfo->m_pName, b: "..") == 0 && (pSelf->m_aCurrentDemoFolder[0] == '\0' || (!pSelf->m_DemolistMultipleStorages && str_comp(a: pSelf->m_aCurrentDemoFolder, b: "demos") == 0))) ||
924 (!IsDir && !str_endswith(str: pInfo->m_pName, suffix: ".demo")))
925 {
926 return 0;
927 }
928
929 CDemoItem Item;
930 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
931 if(IsDir)
932 {
933 str_format(buffer: Item.m_aName, buffer_size: sizeof(Item.m_aName), format: "%s/", pInfo->m_pName);
934 Item.m_Date = 0;
935 }
936 else
937 {
938 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - str_length(str: ".demo"));
939 Item.m_Date = pInfo->m_TimeModified;
940 }
941 Item.m_InfosLoaded = false;
942 Item.m_Valid = false;
943 Item.m_IsDir = IsDir != 0;
944 Item.m_IsLink = false;
945 Item.m_StorageType = StorageType;
946 pSelf->m_vDemos.push_back(x: Item);
947
948 if(time_get_nanoseconds() - pSelf->m_DemoPopulateStartTime > 500ms)
949 {
950 pSelf->RenderLoading(pCaption: Localize(pStr: "Loading demo files"), pContent: "", IncreaseCounter: 0);
951 }
952
953 return 0;
954}
955
956void CMenus::DemolistPopulate()
957{
958 m_vDemos.clear();
959
960 int NumStoragesWithDemos = 0;
961 for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
962 {
963 if(Storage()->FolderExists(pFilename: "demos", Type: StorageType))
964 {
965 NumStoragesWithDemos++;
966 }
967 }
968 m_DemolistMultipleStorages = NumStoragesWithDemos > 1;
969
970 if(m_aCurrentDemoFolder[0] == '\0')
971 {
972 {
973 CDemoItem Item;
974 str_copy(dst&: Item.m_aFilename, src: "demos");
975 str_copy(dst&: Item.m_aName, src: Localize(pStr: "All combined"));
976 Item.m_InfosLoaded = false;
977 Item.m_Valid = false;
978 Item.m_Date = 0;
979 Item.m_IsDir = true;
980 Item.m_IsLink = true;
981 Item.m_StorageType = IStorage::TYPE_ALL;
982 m_vDemos.push_back(x: Item);
983 }
984
985 for(int StorageType = IStorage::TYPE_SAVE; StorageType < Storage()->NumPaths(); ++StorageType)
986 {
987 if(Storage()->FolderExists(pFilename: "demos", Type: StorageType))
988 {
989 CDemoItem Item;
990 str_copy(dst&: Item.m_aFilename, src: "demos");
991 Storage()->GetCompletePath(Type: StorageType, pDir: "demos", pBuffer: Item.m_aName, BufferSize: sizeof(Item.m_aName));
992 str_append(dst: Item.m_aName, src: "/", dst_size: sizeof(Item.m_aName));
993 Item.m_InfosLoaded = false;
994 Item.m_Valid = false;
995 Item.m_Date = 0;
996 Item.m_IsDir = true;
997 Item.m_IsLink = true;
998 Item.m_StorageType = StorageType;
999 m_vDemos.push_back(x: Item);
1000 }
1001 }
1002 }
1003 else
1004 {
1005 m_DemoPopulateStartTime = time_get_nanoseconds();
1006 Storage()->ListDirectoryInfo(Type: m_DemolistStorageType, pPath: m_aCurrentDemoFolder, pfnCallback: DemolistFetchCallback, pUser: this);
1007
1008 if(g_Config.m_BrDemoFetchInfo)
1009 FetchAllHeaders();
1010
1011 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1012 }
1013 RefreshFilteredDemos();
1014}
1015
1016void CMenus::RefreshFilteredDemos()
1017{
1018 m_vpFilteredDemos.clear();
1019 for(auto &Demo : m_vDemos)
1020 {
1021 if(str_find_nocase(haystack: Demo.m_aFilename, needle: m_DemoSearchInput.GetString()))
1022 {
1023 m_vpFilteredDemos.push_back(x: &Demo);
1024 }
1025 }
1026}
1027
1028void CMenus::DemolistOnUpdate(bool Reset)
1029{
1030 if(Reset)
1031 {
1032 if(m_vpFilteredDemos.empty())
1033 {
1034 m_DemolistSelectedIndex = -1;
1035 m_aCurrentDemoSelectionName[0] = '\0';
1036 }
1037 else
1038 {
1039 m_DemolistSelectedIndex = 0;
1040 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1041 }
1042 }
1043 else
1044 {
1045 RefreshFilteredDemos();
1046
1047 // search for selected index
1048 m_DemolistSelectedIndex = -1;
1049 int SelectedIndex = -1;
1050 for(const auto &pItem : m_vpFilteredDemos)
1051 {
1052 SelectedIndex++;
1053 if(str_comp(a: m_aCurrentDemoSelectionName, b: pItem->m_aName) == 0)
1054 {
1055 m_DemolistSelectedIndex = SelectedIndex;
1056 break;
1057 }
1058 }
1059 }
1060
1061 if(m_DemolistSelectedIndex >= 0)
1062 m_DemolistSelectedReveal = true;
1063}
1064
1065bool CMenus::FetchHeader(CDemoItem &Item)
1066{
1067 if(!Item.m_InfosLoaded)
1068 {
1069 char aBuffer[IO_MAX_PATH_LENGTH];
1070 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_aCurrentDemoFolder, Item.m_aFilename);
1071 IOHANDLE File;
1072 Item.m_Valid = DemoPlayer()->GetDemoInfo(pStorage: Storage(), pConsole: nullptr, pFilename: aBuffer, StorageType: Item.m_StorageType, pDemoHeader: &Item.m_Info, pTimelineMarkers: &Item.m_TimelineMarkers, pMapInfo: &Item.m_MapInfo, pFile: &File);
1073 Item.m_InfosLoaded = true;
1074
1075 if(Item.m_Valid && File)
1076 {
1077 Item.m_Size = io_length(io: File);
1078 io_close(io: File);
1079 }
1080 }
1081 return Item.m_Valid;
1082}
1083
1084void CMenus::FetchAllHeaders()
1085{
1086 for(auto &Item : m_vDemos)
1087 {
1088 FetchHeader(Item);
1089 }
1090 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1091}
1092
1093void CMenus::RenderDemoBrowser(CUIRect MainView)
1094{
1095 GameClient()->m_MenuBackground.ChangePosition(PositionNumber: CMenuBackground::POS_DEMOS);
1096
1097 CUIRect ListView, DetailsView, ButtonsView;
1098 MainView.Draw(Color: ms_ColorTabbarActive, Corners: IGraphics::CORNER_B, Rounding: 10.0f);
1099 MainView.Margin(Cut: 10.0f, pOtherRect: &MainView);
1100 MainView.HSplitBottom(Cut: 22.0f * 2.0f + 5.0f, pTop: &ListView, pBottom: &ButtonsView);
1101 ListView.VSplitRight(Cut: 205.0f, pLeft: &ListView, pRight: &DetailsView);
1102 ListView.VSplitRight(Cut: 5.0f, pLeft: &ListView, pRight: nullptr);
1103
1104 bool WasListboxItemActivated;
1105 RenderDemoBrowserList(ListView, WasListboxItemActivated);
1106 RenderDemoBrowserDetails(DetailsView);
1107 RenderDemoBrowserButtons(ButtonsView, WasListboxItemActivated);
1108}
1109
1110void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivated)
1111{
1112 if(!m_DemoBrowserListInitialized)
1113 {
1114 DemolistPopulate();
1115 DemolistOnUpdate(Reset: true);
1116 m_DemoBrowserListInitialized = true;
1117 }
1118
1119#if defined(CONF_VIDEORECORDER)
1120 if(!m_DemoRenderInput.IsEmpty())
1121 {
1122 if(DemoPlayer()->ErrorMessage()[0] == '\0')
1123 {
1124 m_Popup = POPUP_RENDER_DONE;
1125 }
1126 else
1127 {
1128 m_DemoRenderInput.Clear();
1129 }
1130 }
1131#endif
1132
1133 class CColumn
1134 {
1135 public:
1136 int m_Id;
1137 int m_Sort;
1138 const char *m_pCaption;
1139 int m_Direction;
1140 bool m_FontIcon;
1141 float m_Width;
1142 CUIRect m_Rect;
1143 const char *m_pTooltip;
1144 };
1145
1146 enum
1147 {
1148 COL_ICON = 0,
1149 COL_DEMONAME,
1150 COL_MARKERS,
1151 COL_LENGTH,
1152 COL_DATE,
1153 };
1154
1155 static CListBox s_ListBox;
1156 static CColumn s_aCols[] = {
1157 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1158 {.m_Id: COL_ICON, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_FontIcon: false, .m_Width: ms_ListheaderHeight, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1159 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: -1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1160 {.m_Id: COL_DEMONAME, .m_Sort: SORT_DEMONAME, .m_pCaption: Localizable(pStr: "Demo"), .m_Direction: 0, .m_FontIcon: false, .m_Width: 0.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1161 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1162 {.m_Id: COL_MARKERS, .m_Sort: SORT_MARKERS, .m_pCaption: FONT_ICON_BOOKMARK, .m_Direction: 1, .m_FontIcon: true, .m_Width: 30.0f, .m_Rect: {.x: 0}, .m_pTooltip: Localizable(pStr: "Markers")},
1163 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1164 {.m_Id: COL_LENGTH, .m_Sort: SORT_LENGTH, .m_pCaption: Localizable(pStr: "Length"), .m_Direction: 1, .m_FontIcon: false, .m_Width: 75.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1165 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: 2.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1166 {.m_Id: COL_DATE, .m_Sort: SORT_DATE, .m_pCaption: Localizable(pStr: "Date"), .m_Direction: 1, .m_FontIcon: false, .m_Width: 150.0f, .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1167 {.m_Id: -1, .m_Sort: -1, .m_pCaption: "", .m_Direction: 1, .m_FontIcon: false, .m_Width: s_ListBox.ScrollbarWidthMax(), .m_Rect: {.x: 0}, .m_pTooltip: nullptr},
1168 };
1169
1170 CUIRect Headers, ListBox;
1171 ListView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Headers, pBottom: &ListBox);
1172 Headers.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1173 ListBox.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1174
1175 for(auto &Col : s_aCols)
1176 {
1177 if(Col.m_Direction == -1)
1178 {
1179 Headers.VSplitLeft(Cut: Col.m_Width, pLeft: &Col.m_Rect, pRight: &Headers);
1180 }
1181 }
1182
1183 for(int i = std::size(s_aCols) - 1; i >= 0; i--)
1184 {
1185 if(s_aCols[i].m_Direction == 1)
1186 {
1187 Headers.VSplitRight(Cut: s_aCols[i].m_Width, pLeft: &Headers, pRight: &s_aCols[i].m_Rect);
1188 }
1189 }
1190
1191 for(auto &Col : s_aCols)
1192 {
1193 if(Col.m_Direction == 0)
1194 Col.m_Rect = Headers;
1195 }
1196
1197 for(auto &Col : s_aCols)
1198 {
1199 if(Col.m_pCaption[0] != '\0' && Col.m_Sort != -1)
1200 {
1201 if(Col.m_FontIcon)
1202 {
1203 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1204 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1205 }
1206 const int ButtonPressed = DoButton_GridHeader(pId: &Col.m_Id, pText: Col.m_FontIcon ? Col.m_pCaption : Localize(pStr: Col.m_pCaption), Checked: g_Config.m_BrDemoSort == Col.m_Sort, pRect: &Col.m_Rect, Align: Col.m_FontIcon ? TEXTALIGN_MC : TEXTALIGN_ML);
1207 if(Col.m_pTooltip != nullptr)
1208 {
1209 GameClient()->m_Tooltips.DoToolTip(pId: &Col.m_Id, pNearRect: &Col.m_Rect, pText: Localize(pStr: Col.m_pTooltip));
1210 }
1211 if(Col.m_FontIcon)
1212 {
1213 TextRender()->SetRenderFlags(0);
1214 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1215 }
1216 if(ButtonPressed)
1217 {
1218 if(g_Config.m_BrDemoSort == Col.m_Sort)
1219 g_Config.m_BrDemoSortOrder ^= 1;
1220 else
1221 g_Config.m_BrDemoSortOrder = 0;
1222 g_Config.m_BrDemoSort = Col.m_Sort;
1223 // Don't rescan in order to keep fetched headers, just resort
1224 std::stable_sort(first: m_vDemos.begin(), last: m_vDemos.end());
1225 DemolistOnUpdate(Reset: false);
1226 }
1227 }
1228 }
1229
1230 if(m_DemolistSelectedReveal)
1231 {
1232 s_ListBox.ScrollToSelected();
1233 m_DemolistSelectedReveal = false;
1234 }
1235
1236 s_ListBox.DoStart(RowHeight: ms_ListheaderHeight, NumItems: m_vpFilteredDemos.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: m_DemolistSelectedIndex, pRect: &ListBox, Background: false, BackgroundCorners: IGraphics::CORNER_ALL, ForceShowScrollbar: true);
1237
1238 char aBuf[64];
1239 int ItemIndex = -1;
1240 for(auto &pItem : m_vpFilteredDemos)
1241 {
1242 ItemIndex++;
1243
1244 const CListboxItem ListItem = s_ListBox.DoNextItem(pId: pItem, Selected: ItemIndex == m_DemolistSelectedIndex);
1245 if(!ListItem.m_Visible)
1246 continue;
1247
1248 for(const auto &Col : s_aCols)
1249 {
1250 CUIRect Button;
1251 Button.x = Col.m_Rect.x;
1252 Button.y = ListItem.m_Rect.y;
1253 Button.h = ListItem.m_Rect.h;
1254 Button.w = Col.m_Rect.w;
1255
1256 if(Col.m_Id == COL_ICON)
1257 {
1258 Button.Margin(Cut: 1.0f, pOtherRect: &Button);
1259
1260 const char *pIconType;
1261 if(pItem->m_IsLink || str_comp(a: pItem->m_aFilename, b: "..") == 0)
1262 pIconType = FONT_ICON_FOLDER_TREE;
1263 else if(pItem->m_IsDir)
1264 pIconType = FONT_ICON_FOLDER;
1265 else
1266 pIconType = FONT_ICON_FILM;
1267
1268 ColorRGBA IconColor;
1269 if(!pItem->m_IsDir && (!pItem->m_InfosLoaded || !pItem->m_Valid))
1270 IconColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); // not loaded
1271 else
1272 IconColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
1273
1274 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1275 TextRender()->TextColor(Color: IconColor);
1276 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
1277 Ui()->DoLabel(pRect: &Button, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
1278 TextRender()->SetRenderFlags(0);
1279 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
1280 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1281 }
1282 else if(Col.m_Id == COL_DEMONAME)
1283 {
1284 SLabelProperties Props;
1285 Props.m_MaxWidth = Button.w;
1286 Props.m_EllipsisAtEnd = true;
1287 Props.m_EnableWidthCheck = false;
1288 Ui()->DoLabel(pRect: &Button, pText: pItem->m_aName, Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1289 }
1290 else if(Col.m_Id == COL_MARKERS && !pItem->m_IsDir && pItem->m_Valid)
1291 {
1292 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1293 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1294 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1295 }
1296 else if(Col.m_Id == COL_LENGTH && !pItem->m_IsDir && pItem->m_Valid)
1297 {
1298 str_time(centisecs: (int64_t)pItem->Length() * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1299 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1300 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1301 }
1302 else if(Col.m_Id == COL_DATE && !pItem->m_IsDir)
1303 {
1304 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), FORMAT_SPACE);
1305 Button.VMargin(Cut: 4.0f, pOtherRect: &Button);
1306 Ui()->DoLabel(pRect: &Button, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_MR);
1307 }
1308 }
1309 }
1310
1311 const int NewSelected = s_ListBox.DoEnd();
1312 if(NewSelected != m_DemolistSelectedIndex)
1313 {
1314 m_DemolistSelectedIndex = NewSelected;
1315 if(m_DemolistSelectedIndex >= 0)
1316 str_copy(dst&: m_aCurrentDemoSelectionName, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aName);
1317 DemolistOnUpdate(Reset: false);
1318 }
1319
1320 WasListboxItemActivated = s_ListBox.WasItemActivated();
1321}
1322
1323void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView)
1324{
1325 CUIRect Contents, Header;
1326 DetailsView.HSplitTop(Cut: ms_ListheaderHeight, pTop: &Header, pBottom: &Contents);
1327 Contents.Draw(Color: ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_B, Rounding: 5.0f);
1328 Contents.Margin(Cut: 5.0f, pOtherRect: &Contents);
1329
1330 const float FontSize = 12.0f;
1331 CDemoItem *pItem = m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex] : nullptr;
1332
1333 Header.Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), Corners: IGraphics::CORNER_T, Rounding: 5.0f);
1334 const char *pHeaderLabel;
1335 if(pItem == nullptr)
1336 pHeaderLabel = Localize(pStr: "No demo selected");
1337 else if(str_comp(a: pItem->m_aFilename, b: "..") == 0)
1338 pHeaderLabel = Localize(pStr: "Parent Folder");
1339 else if(pItem->m_IsLink)
1340 pHeaderLabel = Localize(pStr: "Folder Link");
1341 else if(pItem->m_IsDir)
1342 pHeaderLabel = Localize(pStr: "Folder");
1343 else if(!FetchHeader(Item&: *pItem))
1344 pHeaderLabel = Localize(pStr: "Invalid Demo");
1345 else
1346 pHeaderLabel = Localize(pStr: "Demo");
1347 Ui()->DoLabel(pRect: &Header, pText: pHeaderLabel, Size: FontSize + 2.0f, Align: TEXTALIGN_MC);
1348
1349 if(pItem == nullptr || pItem->m_IsDir)
1350 return;
1351
1352 char aBuf[256];
1353 CUIRect Left, Right;
1354
1355 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1356 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1357 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Created"), Size: FontSize, Align: TEXTALIGN_ML);
1358 if(pItem->m_Valid)
1359 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Size"), Size: FontSize, Align: TEXTALIGN_ML);
1360 str_timestamp_ex(time: pItem->m_Date, buffer: aBuf, buffer_size: sizeof(aBuf), FORMAT_SPACE);
1361 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1362 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1363 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1364
1365 if(!pItem->m_Valid)
1366 return;
1367
1368 const float DemoSize = pItem->m_Size / 1024.0f;
1369 if(DemoSize > 1024)
1370 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), DemoSize / 1024.0f);
1371 else
1372 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), DemoSize);
1373 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1374 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1375
1376 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1377 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1378 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Type"), Size: FontSize, Align: TEXTALIGN_ML);
1379 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Version"), Size: FontSize, Align: TEXTALIGN_ML);
1380 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1381 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1382 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aType, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1383 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->m_Info.m_Version);
1384 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1385 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1386
1387 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1388 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1389 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Length"), Size: FontSize, Align: TEXTALIGN_ML);
1390 Ui()->DoLabel(pRect: &Right, pText: Localize(pStr: "Markers"), Size: FontSize, Align: TEXTALIGN_ML);
1391 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1392 Left.VSplitLeft(Cut: Contents.w / 2.f + 30.f, pLeft: &Left, pRight: &Right);
1393 str_time(centisecs: (int64_t)pItem->Length() * 100, format: TIME_HOURS, buffer: aBuf, buffer_size: sizeof(aBuf));
1394 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1395 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", pItem->NumMarkers());
1396 Ui()->DoLabel(pRect: &Right, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1397 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1398
1399 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1400 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Netversion"), Size: FontSize, Align: TEXTALIGN_ML);
1401 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1402 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aNetversion, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1403 Contents.HSplitTop(Cut: 16.0f, pTop: nullptr, pBottom: &Contents);
1404
1405 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1406 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map"), Size: FontSize, Align: TEXTALIGN_ML);
1407 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1408 Ui()->DoLabel(pRect: &Left, pText: pItem->m_Info.m_aMapName, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1409 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1410
1411 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1412 Ui()->DoLabel(pRect: &Left, pText: Localize(pStr: "Map size"), Size: FontSize, Align: TEXTALIGN_ML);
1413 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1414 const float MapSize = pItem->MapSize() / 1024.0f;
1415 if(MapSize == 0.0f)
1416 str_copy(dst&: aBuf, src: Localize(pStr: "map not included", pContext: "Demo details"));
1417 else if(MapSize > 1024)
1418 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f MiB"), MapSize / 1024.0f);
1419 else
1420 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%.2f KiB"), MapSize);
1421 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1422 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1423
1424 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1425 if(pItem->m_MapInfo.m_Sha256 != SHA256_ZEROED)
1426 {
1427 Ui()->DoLabel(pRect: &Left, pText: "SHA256", Size: FontSize, Align: TEXTALIGN_ML);
1428 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1429 char aSha[SHA256_MAXSTRSIZE];
1430 sha256_str(digest: pItem->m_MapInfo.m_Sha256, str: aSha, max_len: sizeof(aSha));
1431 SLabelProperties Props;
1432 Props.m_MaxWidth = Left.w;
1433 Props.m_EllipsisAtEnd = true;
1434 Props.m_EnableWidthCheck = false;
1435 Ui()->DoLabel(pRect: &Left, pText: aSha, Size: FontSize - 1.0f, Align: TEXTALIGN_ML, LabelProps: Props);
1436 }
1437 else
1438 {
1439 Ui()->DoLabel(pRect: &Left, pText: "CRC32", Size: FontSize, Align: TEXTALIGN_ML);
1440 Contents.HSplitTop(Cut: 18.0f, pTop: &Left, pBottom: &Contents);
1441 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%08x", pItem->m_MapInfo.m_Crc);
1442 Ui()->DoLabel(pRect: &Left, pText: aBuf, Size: FontSize - 1.0f, Align: TEXTALIGN_ML);
1443 }
1444 Contents.HSplitTop(Cut: 4.0f, pTop: nullptr, pBottom: &Contents);
1445}
1446
1447void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemActivated)
1448{
1449 const auto &&SetIconMode = [&](bool Enable) {
1450 if(Enable)
1451 {
1452 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
1453 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_ALIGNMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE);
1454 }
1455 else
1456 {
1457 TextRender()->SetRenderFlags(0);
1458 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
1459 }
1460 };
1461
1462 CUIRect ButtonBarTop, ButtonBarBottom;
1463 ButtonsView.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &ButtonsView);
1464 ButtonsView.HSplitMid(pTop: &ButtonBarTop, pBottom: &ButtonBarBottom, Spacing: 5.0f);
1465
1466 // quick search
1467 {
1468 CUIRect DemoSearch;
1469 ButtonBarTop.VSplitLeft(Cut: ButtonBarBottom.h * 21.0f, pLeft: &DemoSearch, pRight: &ButtonBarTop);
1470 ButtonBarTop.VSplitLeft(Cut: ButtonBarTop.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarTop);
1471 if(Ui()->DoEditBox_Search(pLineInput: &m_DemoSearchInput, pRect: &DemoSearch, FontSize: 14.0f, HotkeyEnabled: !Ui()->IsPopupOpen() && !GameClient()->m_GameConsole.IsActive()))
1472 {
1473 RefreshFilteredDemos();
1474 DemolistOnUpdate(Reset: false);
1475 }
1476 }
1477
1478 // refresh button
1479 {
1480 CUIRect RefreshButton;
1481 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 3.0f, pLeft: &RefreshButton, pRight: &ButtonBarBottom);
1482 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1483 SetIconMode(true);
1484 static CButtonContainer s_RefreshButton;
1485 if(DoButton_Menu(pButtonContainer: &s_RefreshButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: 0, pRect: &RefreshButton) || Input()->KeyPress(Key: KEY_F5) || (Input()->KeyPress(Key: KEY_R) && Input()->ModifierIsPressed()))
1486 {
1487 SetIconMode(false);
1488 DemolistPopulate();
1489 DemolistOnUpdate(Reset: false);
1490 }
1491 SetIconMode(false);
1492 }
1493
1494 // fetch info checkbox
1495 {
1496 CUIRect FetchInfo;
1497 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 7.0f, pLeft: &FetchInfo, pRight: &ButtonBarBottom);
1498 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1499 if(DoButton_CheckBox(pId: &g_Config.m_BrDemoFetchInfo, pText: Localize(pStr: "Fetch Info"), Checked: g_Config.m_BrDemoFetchInfo, pRect: &FetchInfo))
1500 {
1501 g_Config.m_BrDemoFetchInfo ^= 1;
1502 if(g_Config.m_BrDemoFetchInfo)
1503 FetchAllHeaders();
1504 }
1505 }
1506
1507 // demos directory button
1508 if(m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
1509 {
1510 CUIRect DemosDirectoryButton;
1511 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h * 10.0f, pLeft: &DemosDirectoryButton, pRight: &ButtonBarBottom);
1512 ButtonBarBottom.VSplitLeft(Cut: ButtonBarBottom.h / 2.0f, pLeft: nullptr, pRight: &ButtonBarBottom);
1513 static CButtonContainer s_DemosDirectoryButton;
1514 if(DoButton_Menu(pButtonContainer: &s_DemosDirectoryButton, pText: Localize(pStr: "Demos directory"), Checked: 0, pRect: &DemosDirectoryButton))
1515 {
1516 char aBuf[IO_MAX_PATH_LENGTH];
1517 Storage()->GetCompletePath(Type: m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, pDir: m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, pBuffer: aBuf, BufferSize: sizeof(aBuf));
1518 Client()->ViewFile(pFilename: aBuf);
1519 }
1520 GameClient()->m_Tooltips.DoToolTip(pId: &s_DemosDirectoryButton, pNearRect: &DemosDirectoryButton, pText: Localize(pStr: "Open the directory that contains the demo files"));
1521 }
1522
1523 // play/open button
1524 if(m_DemolistSelectedIndex >= 0)
1525 {
1526 CUIRect PlayButton;
1527 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &PlayButton);
1528 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarBottom, pRight: nullptr);
1529 SetIconMode(true);
1530 static CButtonContainer s_PlayButton;
1531 if(DoButton_Menu(pButtonContainer: &s_PlayButton, pText: (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? FONT_ICON_FOLDER_OPEN : FONT_ICON_PLAY, Checked: 0, pRect: &PlayButton) || WasListboxItemActivated || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || (Input()->KeyPress(Key: KEY_P) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive()))
1532 {
1533 SetIconMode(false);
1534 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder
1535 {
1536 m_DemoSearchInput.Clear();
1537 const bool ParentFolder = str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") == 0;
1538 if(ParentFolder) // parent folder
1539 {
1540 str_copy(dst&: m_aCurrentDemoSelectionName, src: fs_filename(path: m_aCurrentDemoFolder));
1541 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1542 if(fs_parent_dir(path: m_aCurrentDemoFolder))
1543 {
1544 m_aCurrentDemoFolder[0] = '\0';
1545 if(m_DemolistStorageType == IStorage::TYPE_ALL)
1546 {
1547 m_aCurrentDemoSelectionName[0] = '\0'; // will select first list item
1548 }
1549 else
1550 {
1551 Storage()->GetCompletePath(Type: m_DemolistStorageType, pDir: "demos", pBuffer: m_aCurrentDemoSelectionName, BufferSize: sizeof(m_aCurrentDemoSelectionName));
1552 str_append(dst&: m_aCurrentDemoSelectionName, src: "/");
1553 }
1554 }
1555 }
1556 else // sub folder
1557 {
1558 if(m_aCurrentDemoFolder[0] != '\0')
1559 str_append(dst&: m_aCurrentDemoFolder, src: "/");
1560 else
1561 m_DemolistStorageType = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType;
1562 str_append(dst&: m_aCurrentDemoFolder, src: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1563 }
1564 DemolistPopulate();
1565 DemolistOnUpdate(Reset: !ParentFolder);
1566 }
1567 else // file
1568 {
1569 if(GameClient()->CurrentRaceTime() / 60 >= g_Config.m_ClConfirmDisconnectTime && g_Config.m_ClConfirmDisconnectTime >= 0)
1570 PopupConfirm(pTitle: Localize(pStr: "Disconnect"), pMessage: Localize(pStr: "Are you sure that you want to disconnect and play this demo?"), pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: &CMenus::PopupConfirmPlayDemo);
1571 else
1572 CMenus::PopupConfirmPlayDemo();
1573 return;
1574 }
1575 }
1576 SetIconMode(false);
1577
1578 if(m_aCurrentDemoFolder[0] != '\0')
1579 {
1580 if(str_comp(a: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, b: "..") != 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE)
1581 {
1582 // rename button
1583 CUIRect RenameButton;
1584 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &RenameButton);
1585 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1586 SetIconMode(true);
1587 static CButtonContainer s_RenameButton;
1588 if(DoButton_Menu(pButtonContainer: &s_RenameButton, pText: FONT_ICON_PENCIL, Checked: 0, pRect: &RenameButton))
1589 {
1590 SetIconMode(false);
1591 m_Popup = POPUP_RENAME_DEMO;
1592 if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1593 {
1594 m_DemoRenameInput.Set(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1595 }
1596 else
1597 {
1598 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1599 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1600 m_DemoRenameInput.Set(aNameWithoutExt);
1601 }
1602 Ui()->SetActiveItem(&m_DemoRenameInput);
1603 return;
1604 }
1605
1606 // delete button
1607 static CButtonContainer s_DeleteButton;
1608 CUIRect DeleteButton;
1609 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarBottom, pRight: &DeleteButton);
1610 ButtonBarBottom.VSplitRight(Cut: ButtonBarBottom.h / 2.0f, pLeft: &ButtonBarBottom, pRight: nullptr);
1611 if(DoButton_Menu(pButtonContainer: &s_DeleteButton, pText: FONT_ICON_TRASH, Checked: 0, pRect: &DeleteButton) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE) || (Input()->KeyPress(Key: KEY_D) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive()))
1612 {
1613 SetIconMode(false);
1614 char aBuf[128 + IO_MAX_PATH_LENGTH];
1615 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Are you sure that you want to delete the folder '%s'?") : Localize(pStr: "Are you sure that you want to delete the demo '%s'?"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1616 PopupConfirm(pTitle: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize(pStr: "Delete folder") : Localize(pStr: "Delete demo"), pMessage: aBuf, pConfirmButtonLabel: Localize(pStr: "Yes"), pCancelButtonLabel: Localize(pStr: "No"), pfnConfirmButtonCallback: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? &CMenus::PopupConfirmDeleteFolder : &CMenus::PopupConfirmDeleteDemo);
1617 return;
1618 }
1619 SetIconMode(false);
1620 }
1621
1622#if defined(CONF_VIDEORECORDER)
1623 // render demo button
1624 if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir)
1625 {
1626 CUIRect RenderButton;
1627 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h * 3.0f, pLeft: &ButtonBarTop, pRight: &RenderButton);
1628 ButtonBarTop.VSplitRight(Cut: ButtonBarBottom.h, pLeft: &ButtonBarTop, pRight: nullptr);
1629 SetIconMode(true);
1630 static CButtonContainer s_RenderButton;
1631 if(DoButton_Menu(pButtonContainer: &s_RenderButton, pText: FONT_ICON_VIDEO, Checked: 0, pRect: &RenderButton) || (Input()->KeyPress(Key: KEY_R) && !GameClient()->m_GameConsole.IsActive() && !m_DemoSearchInput.IsActive()))
1632 {
1633 SetIconMode(false);
1634 m_Popup = POPUP_RENDER_DEMO;
1635 m_StartPaused = false;
1636 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
1637 fs_split_file_extension(filename: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
1638 m_DemoRenderInput.Set(aNameWithoutExt);
1639 Ui()->SetActiveItem(&m_DemoRenderInput);
1640 return;
1641 }
1642 SetIconMode(false);
1643 }
1644#endif
1645 }
1646 }
1647}
1648
1649void CMenus::PopupConfirmPlayDemo()
1650{
1651 char aBuf[IO_MAX_PATH_LENGTH];
1652 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1653 const char *pError = Client()->DemoPlayer_Play(pFilename: aBuf, StorageType: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType);
1654 m_LastPauseChange = -1.0f;
1655 m_LastSpeedChange = -1.0f;
1656 if(pError)
1657 {
1658 PopupMessage(pTitle: Localize(pStr: "Error loading demo"), pMessage: pError, pButtonLabel: Localize(pStr: "Ok"));
1659 }
1660 else
1661 {
1662 Ui()->SetActiveItem(nullptr);
1663 return;
1664 }
1665}
1666
1667void CMenus::PopupConfirmDeleteDemo()
1668{
1669 char aBuf[IO_MAX_PATH_LENGTH];
1670 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1671 if(Storage()->RemoveFile(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1672 {
1673 DemolistPopulate();
1674 DemolistOnUpdate(Reset: false);
1675 }
1676 else
1677 {
1678 char aError[128 + IO_MAX_PATH_LENGTH];
1679 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the demo '%s'"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1680 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1681 }
1682}
1683
1684void CMenus::PopupConfirmDeleteFolder()
1685{
1686 char aBuf[IO_MAX_PATH_LENGTH];
1687 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1688 if(Storage()->RemoveFolder(pFilename: aBuf, Type: m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType))
1689 {
1690 DemolistPopulate();
1691 DemolistOnUpdate(Reset: false);
1692 }
1693 else
1694 {
1695 char aError[128 + IO_MAX_PATH_LENGTH];
1696 str_format(buffer: aError, buffer_size: sizeof(aError), format: Localize(pStr: "Unable to delete the folder '%s'. Make sure it's empty first."), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename);
1697 PopupMessage(pTitle: Localize(pStr: "Error"), pMessage: aError, pButtonLabel: Localize(pStr: "Ok"));
1698 }
1699}
1700
1701void CMenus::ConchainDemoPlay(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1702{
1703 CMenus *pThis = static_cast<CMenus *>(pUserData);
1704 pThis->m_LastPauseChange = pThis->Client()->GlobalTime();
1705 pfnCallback(pResult, pCallbackUserData);
1706}
1707
1708void CMenus::ConchainDemoSpeed(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
1709{
1710 CMenus *pThis = static_cast<CMenus *>(pUserData);
1711 if(pResult->NumArguments() == 1)
1712 {
1713 pThis->m_LastSpeedChange = pThis->Client()->GlobalTime();
1714 }
1715 pfnCallback(pResult, pCallbackUserData);
1716}
1717