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 <algorithm>
5
6#include <base/color.h>
7#include <base/system.h>
8
9#if defined(CONF_FAMILY_UNIX)
10#include <pthread.h>
11#endif
12
13#include <engine/client.h>
14#include <engine/console.h>
15#include <engine/gfx/image_loader.h>
16#include <engine/gfx/image_manipulation.h>
17#include <engine/graphics.h>
18#include <engine/input.h>
19#include <engine/keys.h>
20#include <engine/shared/config.h>
21#include <engine/shared/filecollection.h>
22#include <engine/storage.h>
23#include <engine/textrender.h>
24
25#include <game/client/components/camera.h>
26#include <game/client/gameclient.h>
27#include <game/client/lineinput.h>
28#include <game/client/render.h>
29#include <game/client/ui.h>
30#include <game/client/ui_listbox.h>
31#include <game/client/ui_scrollregion.h>
32#include <game/generated/client_data.h>
33#include <game/localization.h>
34
35#include <game/editor/editor_history.h>
36#include <game/editor/mapitems/image.h>
37#include <game/editor/mapitems/sound.h>
38
39#include "auto_map.h"
40#include "editor.h"
41#include "editor_actions.h"
42
43#include <chrono>
44#include <iterator>
45#include <limits>
46#include <type_traits>
47
48using namespace FontIcons;
49
50static const char *VANILLA_IMAGES[] = {
51 "bg_cloud1",
52 "bg_cloud2",
53 "bg_cloud3",
54 "desert_doodads",
55 "desert_main",
56 "desert_mountains",
57 "desert_mountains2",
58 "desert_sun",
59 "generic_deathtiles",
60 "generic_unhookable",
61 "grass_doodads",
62 "grass_main",
63 "jungle_background",
64 "jungle_deathtiles",
65 "jungle_doodads",
66 "jungle_main",
67 "jungle_midground",
68 "jungle_unhookables",
69 "moon",
70 "mountains",
71 "snow",
72 "stars",
73 "sun",
74 "winter_doodads",
75 "winter_main",
76 "winter_mountains",
77 "winter_mountains2",
78 "winter_mountains3"};
79
80bool CEditor::IsVanillaImage(const char *pImage)
81{
82 return std::any_of(first: std::begin(arr&: VANILLA_IMAGES), last: std::end(arr&: VANILLA_IMAGES), pred: [pImage](const char *pVanillaImage) { return str_comp(a: pImage, b: pVanillaImage) == 0; });
83}
84
85static const char *FILETYPE_EXTENSIONS[CEditor::NUM_FILETYPES] = {
86 ".map",
87 ".png",
88 ".opus"};
89
90const void *CEditor::ms_pUiGotContext;
91
92enum
93{
94 BUTTON_CONTEXT = 1,
95};
96
97void CEditor::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser)
98{
99 CEditor *pThis = (CEditor *)pUser;
100 if(Env < 0 || Env >= (int)pThis->m_Map.m_vpEnvelopes.size())
101 return;
102
103 std::shared_ptr<CEnvelope> pEnv = pThis->m_Map.m_vpEnvelopes[Env];
104 float Time = pThis->m_AnimateTime;
105 Time *= pThis->m_AnimateSpeed;
106 Time += (TimeOffsetMillis / 1000.0f);
107 pEnv->Eval(Time, Result, Channels);
108}
109
110/********************************************************
111 OTHER
112*********************************************************/
113
114bool CEditor::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector<STextColorSplit> &vColorSplits)
115{
116 UpdateTooltip(pId: pLineInput, pRect, pToolTip);
117 return Ui()->DoEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits);
118}
119
120bool CEditor::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector<STextColorSplit> &vColorSplits)
121{
122 UpdateTooltip(pId: pLineInput, pRect, pToolTip);
123 return Ui()->DoClearableEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits);
124}
125
126ColorRGBA CEditor::GetButtonColor(const void *pId, int Checked)
127{
128 if(Checked < 0)
129 return ColorRGBA(0, 0, 0, 0.5f);
130
131 switch(Checked)
132 {
133 case 8: // invisible
134 return ColorRGBA(0, 0, 0, 0);
135 case 7: // selected + game layers
136 if(Ui()->HotItem() == pId)
137 return ColorRGBA(1, 0, 0, 0.4f);
138 return ColorRGBA(1, 0, 0, 0.2f);
139
140 case 6: // game layers
141 if(Ui()->HotItem() == pId)
142 return ColorRGBA(1, 1, 1, 0.4f);
143 return ColorRGBA(1, 1, 1, 0.2f);
144
145 case 5: // selected + image/sound should be embedded
146 if(Ui()->HotItem() == pId)
147 return ColorRGBA(1, 0, 0, 0.75f);
148 return ColorRGBA(1, 0, 0, 0.5f);
149
150 case 4: // image/sound should be embedded
151 if(Ui()->HotItem() == pId)
152 return ColorRGBA(1, 0, 0, 1.0f);
153 return ColorRGBA(1, 0, 0, 0.875f);
154
155 case 3: // selected + unused image/sound
156 if(Ui()->HotItem() == pId)
157 return ColorRGBA(1, 0, 1, 0.75f);
158 return ColorRGBA(1, 0, 1, 0.5f);
159
160 case 2: // unused image/sound
161 if(Ui()->HotItem() == pId)
162 return ColorRGBA(0, 0, 1, 0.75f);
163 return ColorRGBA(0, 0, 1, 0.5f);
164
165 case 1: // selected
166 if(Ui()->HotItem() == pId)
167 return ColorRGBA(1, 0, 0, 0.75f);
168 return ColorRGBA(1, 0, 0, 0.5f);
169
170 default: // regular
171 if(Ui()->HotItem() == pId)
172 return ColorRGBA(1, 1, 1, 0.75f);
173 return ColorRGBA(1, 1, 1, 0.5f);
174 }
175}
176
177void CEditor::UpdateTooltip(const void *pId, const CUIRect *pRect, const char *pToolTip)
178{
179 if(Ui()->MouseInside(pRect) && !pToolTip)
180 str_copy(dst&: m_aTooltip, src: "");
181 else if(Ui()->HotItem() == pId && pToolTip)
182 str_copy(dst&: m_aTooltip, src: pToolTip);
183}
184
185int CEditor::DoButton_Editor_Common(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip)
186{
187 if(Ui()->MouseInside(pRect))
188 {
189 if(Flags & BUTTON_CONTEXT)
190 ms_pUiGotContext = pId;
191 }
192
193 UpdateTooltip(pId, pRect, pToolTip);
194 return Ui()->DoButtonLogic(pId, Checked, pRect);
195}
196
197int CEditor::DoButton_Editor(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip)
198{
199 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
200 CUIRect NewRect = *pRect;
201 Ui()->DoLabel(pRect: &NewRect, pText, Size: 10.0f, Align: TEXTALIGN_MC);
202 Checked %= 2;
203 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
204}
205
206int CEditor::DoButton_Env(const void *pId, const char *pText, int Checked, const CUIRect *pRect, const char *pToolTip, ColorRGBA BaseColor, int Corners)
207{
208 float Bright = Checked ? 1.0f : 0.5f;
209 float Alpha = Ui()->HotItem() == pId ? 1.0f : 0.75f;
210 ColorRGBA Color = ColorRGBA(BaseColor.r * Bright, BaseColor.g * Bright, BaseColor.b * Bright, Alpha);
211
212 pRect->Draw(Color, Corners, Rounding: 3.0f);
213 Ui()->DoLabel(pRect, pText, Size: 10.0f, Align: TEXTALIGN_MC);
214 Checked %= 2;
215 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags: 0, pToolTip);
216}
217
218int CEditor::DoButton_MenuItem(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip)
219{
220 if(Ui()->HotItem() == pId || Checked)
221 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
222
223 CUIRect Rect;
224 pRect->VMargin(Cut: 5.0f, pOtherRect: &Rect);
225
226 SLabelProperties Props;
227 Props.m_MaxWidth = Rect.w;
228 Props.m_EllipsisAtEnd = true;
229 Ui()->DoLabel(pRect: &Rect, pText, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
230
231 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
232}
233
234int CEditor::DoButton_Ex(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize, int Align)
235{
236 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners, Rounding: 3.0f);
237
238 CUIRect Rect;
239 pRect->VMargin(Cut: ((Align & TEXTALIGN_MASK_HORIZONTAL) == TEXTALIGN_CENTER) ? 1.0f : 5.0f, pOtherRect: &Rect);
240
241 SLabelProperties Props;
242 Props.m_MaxWidth = Rect.w;
243 Props.m_EllipsisAtEnd = true;
244 Ui()->DoLabel(pRect: &Rect, pText, Size: FontSize, Align, LabelProps: Props);
245
246 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
247}
248
249int CEditor::DoButton_FontIcon(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize)
250{
251 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners, Rounding: 3.0f);
252
253 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
254 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
255 Ui()->DoLabel(pRect, pText, Size: FontSize, Align: TEXTALIGN_MC);
256 TextRender()->SetRenderFlags(0);
257 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
258
259 return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip);
260}
261
262int CEditor::DoButton_DraggableEx(const void *pId, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip, int Corners, float FontSize)
263{
264 pRect->Draw(Color: GetButtonColor(pId, Checked), Corners, Rounding: 3.0f);
265
266 CUIRect Rect;
267 pRect->VMargin(Cut: pRect->w > 20.0f ? 5.0f : 0.0f, pOtherRect: &Rect);
268
269 SLabelProperties Props;
270 Props.m_MaxWidth = Rect.w;
271 Props.m_EllipsisAtEnd = true;
272 Ui()->DoLabel(pRect: &Rect, pText, Size: FontSize, Align: TEXTALIGN_MC, LabelProps: Props);
273
274 if(Ui()->MouseInside(pRect))
275 {
276 if(Flags & BUTTON_CONTEXT)
277 ms_pUiGotContext = pId;
278 }
279
280 UpdateTooltip(pId, pRect, pToolTip);
281 return Ui()->DoDraggableButtonLogic(pId, Checked, pRect, pClicked, pAbrupted);
282}
283
284void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness) const
285{
286 Graphics()->TextureSet(Texture);
287 Graphics()->BlendNormal();
288 Graphics()->QuadsBegin();
289 Graphics()->SetColor(r: Brightness, g: Brightness, b: Brightness, a: 1.0f);
290 Graphics()->QuadsSetSubset(TopLeftU: 0, TopLeftV: 0, BottomRightU: View.w / Size, BottomRightV: View.h / Size);
291 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
292 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
293 Graphics()->QuadsEnd();
294}
295
296SEditResult<int> CEditor::UiDoValueSelector(void *pId, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree, bool IsHex, int Corners, const ColorRGBA *pColor, bool ShowValue)
297{
298 // logic
299 static bool s_DidScroll = false;
300 static float s_ScrollValue = 0.0f;
301 static CLineInputNumber s_NumberInput;
302 static int s_ButtonUsed = -1;
303 static void *s_pLastTextId = nullptr;
304
305 const bool Inside = Ui()->MouseInside(pRect);
306 const int Base = IsHex ? 16 : 10;
307
308 if(Ui()->HotItem() == pId && s_ButtonUsed >= 0 && !Ui()->MouseButton(Index: s_ButtonUsed))
309 {
310 Ui()->DisableMouseLock();
311 if(Ui()->CheckActiveItem(pId))
312 {
313 Ui()->SetActiveItem(nullptr);
314 }
315 if(Inside && ((s_ButtonUsed == 0 && !s_DidScroll && Ui()->DoDoubleClickLogic(pId)) || s_ButtonUsed == 1))
316 {
317 s_pLastTextId = pId;
318 s_NumberInput.SetInteger(Number: Current, Base);
319 s_NumberInput.SelectAll();
320 }
321 s_ButtonUsed = -1;
322 }
323
324 if(s_pLastTextId == pId)
325 {
326 str_copy(dst&: m_aTooltip, src: "Type your number");
327 Ui()->SetActiveItem(&s_NumberInput);
328 DoEditBox(pLineInput: &s_NumberInput, pRect, FontSize: 10.0f, Corners);
329
330 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER) || ((Ui()->MouseButtonClicked(Index: 1) || Ui()->MouseButtonClicked(Index: 0)) && !Inside))
331 {
332 Current = clamp(val: s_NumberInput.GetInteger(Base), lo: Min, hi: Max);
333 Ui()->DisableMouseLock();
334 Ui()->SetActiveItem(nullptr);
335 s_pLastTextId = nullptr;
336 }
337
338 if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
339 {
340 Ui()->DisableMouseLock();
341 Ui()->SetActiveItem(nullptr);
342 s_pLastTextId = nullptr;
343 }
344 }
345 else
346 {
347 if(Ui()->CheckActiveItem(pId))
348 {
349 if(s_ButtonUsed == 0 && Ui()->MouseButton(Index: 0))
350 {
351 s_ScrollValue += Ui()->MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.05f : 1.0f);
352
353 if(absolute(a: s_ScrollValue) >= Scale)
354 {
355 int Count = (int)(s_ScrollValue / Scale);
356 s_ScrollValue = std::fmod(x: s_ScrollValue, y: Scale);
357 Current += Step * Count;
358 Current = clamp(val: Current, lo: Min, hi: Max);
359 s_DidScroll = true;
360
361 // Constrain to discrete steps
362 if(Count > 0)
363 Current = Current / Step * Step;
364 else
365 Current = std::ceil(x: Current / (float)Step) * Step;
366 }
367 }
368
369 if(pToolTip && s_pLastTextId != pId)
370 str_copy(dst&: m_aTooltip, src: pToolTip);
371 }
372 else if(Ui()->HotItem() == pId)
373 {
374 if(Ui()->MouseButton(Index: 0))
375 {
376 s_ButtonUsed = 0;
377 s_DidScroll = false;
378 s_ScrollValue = 0.0f;
379 Ui()->SetActiveItem(pId);
380 Ui()->EnableMouseLock(pId);
381 }
382 else if(Ui()->MouseButton(Index: 1))
383 {
384 s_ButtonUsed = 1;
385 Ui()->SetActiveItem(pId);
386 }
387
388 if(pToolTip && s_pLastTextId != pId)
389 str_copy(dst&: m_aTooltip, src: pToolTip);
390 }
391
392 // render
393 char aBuf[128];
394 if(pLabel[0] != '\0')
395 {
396 if(ShowValue)
397 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s %d", pLabel, Current);
398 else
399 str_copy(dst&: aBuf, src: pLabel);
400 }
401 else if(IsDegree)
402 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d°", Current);
403 else if(IsHex)
404 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%06X", Current);
405 else
406 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Current);
407 pRect->Draw(Color: pColor ? *pColor : GetButtonColor(pId, Checked: 0), Corners, Rounding: 3.0f);
408 Ui()->DoLabel(pRect, pText: aBuf, Size: 10, Align: TEXTALIGN_MC);
409 }
410
411 if(Inside && !Ui()->MouseButton(Index: 0) && !Ui()->MouseButton(Index: 1))
412 Ui()->SetHotItem(pId);
413
414 static const void *s_pEditing = nullptr;
415 EEditState State = EEditState::NONE;
416 if(s_pEditing == pId)
417 {
418 State = EEditState::EDITING;
419 }
420 if(((Ui()->CheckActiveItem(pId) && Ui()->CheckMouseLock()) || s_pLastTextId == pId) && s_pEditing != pId)
421 {
422 State = EEditState::START;
423 s_pEditing = pId;
424 }
425 if(!Ui()->CheckMouseLock() && s_pLastTextId != pId && s_pEditing == pId)
426 {
427 State = EEditState::END;
428 s_pEditing = nullptr;
429 }
430
431 return SEditResult<int>{.m_State: State, .m_Value: Current};
432}
433
434std::shared_ptr<CLayerGroup> CEditor::GetSelectedGroup() const
435{
436 if(m_SelectedGroup >= 0 && m_SelectedGroup < (int)m_Map.m_vpGroups.size())
437 return m_Map.m_vpGroups[m_SelectedGroup];
438 return nullptr;
439}
440
441std::shared_ptr<CLayer> CEditor::GetSelectedLayer(int Index) const
442{
443 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
444 if(!pGroup)
445 return nullptr;
446
447 if(Index < 0 || Index >= (int)m_vSelectedLayers.size())
448 return nullptr;
449
450 int LayerIndex = m_vSelectedLayers[Index];
451
452 if(LayerIndex >= 0 && LayerIndex < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size())
453 return pGroup->m_vpLayers[LayerIndex];
454 return nullptr;
455}
456
457std::shared_ptr<CLayer> CEditor::GetSelectedLayerType(int Index, int Type) const
458{
459 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index);
460 if(pLayer && pLayer->m_Type == Type)
461 return pLayer;
462 return nullptr;
463}
464
465std::vector<CQuad *> CEditor::GetSelectedQuads()
466{
467 std::shared_ptr<CLayerQuads> pQuadLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
468 std::vector<CQuad *> vpQuads;
469 if(!pQuadLayer)
470 return vpQuads;
471 vpQuads.resize(new_size: m_vSelectedQuads.size());
472 for(int i = 0; i < (int)m_vSelectedQuads.size(); ++i)
473 vpQuads[i] = &pQuadLayer->m_vQuads[m_vSelectedQuads[i]];
474 return vpQuads;
475}
476
477CSoundSource *CEditor::GetSelectedSource() const
478{
479 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_SOUNDS));
480 if(!pSounds)
481 return nullptr;
482 if(m_SelectedSource >= 0 && m_SelectedSource < (int)pSounds->m_vSources.size())
483 return &pSounds->m_vSources[m_SelectedSource];
484 return nullptr;
485}
486
487void CEditor::SelectLayer(int LayerIndex, int GroupIndex)
488{
489 if(GroupIndex != -1)
490 m_SelectedGroup = GroupIndex;
491
492 m_vSelectedLayers.clear();
493 DeselectQuads();
494 DeselectQuadPoints();
495 AddSelectedLayer(LayerIndex);
496}
497
498void CEditor::AddSelectedLayer(int LayerIndex)
499{
500 m_vSelectedLayers.push_back(x: LayerIndex);
501
502 m_QuadKnifeActive = false;
503}
504
505void CEditor::SelectQuad(int Index)
506{
507 m_vSelectedQuads.clear();
508 m_vSelectedQuads.push_back(x: Index);
509}
510
511void CEditor::ToggleSelectQuad(int Index)
512{
513 int ListIndex = FindSelectedQuadIndex(Index);
514 if(ListIndex < 0)
515 m_vSelectedQuads.push_back(x: Index);
516 else
517 m_vSelectedQuads.erase(position: m_vSelectedQuads.begin() + ListIndex);
518}
519
520void CEditor::DeselectQuads()
521{
522 m_vSelectedQuads.clear();
523}
524
525void CEditor::DeselectQuadPoints()
526{
527 m_SelectedQuadPoints = 0;
528}
529
530void CEditor::SelectQuadPoint(int QuadIndex, int Index)
531{
532 SelectQuad(Index: QuadIndex);
533 m_SelectedQuadPoints = 1 << Index;
534}
535
536void CEditor::ToggleSelectQuadPoint(int QuadIndex, int Index)
537{
538 if(IsQuadPointSelected(QuadIndex, Index))
539 {
540 m_SelectedQuadPoints ^= 1 << Index;
541 }
542 else
543 {
544 if(!IsQuadSelected(Index: QuadIndex))
545 {
546 ToggleSelectQuad(Index: QuadIndex);
547 }
548
549 if(!(m_SelectedQuadPoints & 1 << Index))
550 {
551 m_SelectedQuadPoints ^= 1 << Index;
552 }
553 }
554}
555
556void CEditor::DeleteSelectedQuads()
557{
558 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
559 if(!pLayer)
560 return;
561
562 std::vector<int> vSelectedQuads(m_vSelectedQuads);
563 std::vector<CQuad> vDeletedQuads;
564
565 for(int i = 0; i < (int)m_vSelectedQuads.size(); ++i)
566 {
567 auto const &Quad = pLayer->m_vQuads[m_vSelectedQuads[i]];
568 vDeletedQuads.push_back(x: Quad);
569
570 pLayer->m_vQuads.erase(position: pLayer->m_vQuads.begin() + m_vSelectedQuads[i]);
571 for(int j = i + 1; j < (int)m_vSelectedQuads.size(); ++j)
572 if(m_vSelectedQuads[j] > m_vSelectedQuads[i])
573 m_vSelectedQuads[j]--;
574
575 m_vSelectedQuads.erase(position: m_vSelectedQuads.begin() + i);
576 i--;
577 }
578
579 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionDeleteQuad>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0], args&: vSelectedQuads, args&: vDeletedQuads));
580}
581
582bool CEditor::IsQuadSelected(int Index) const
583{
584 return FindSelectedQuadIndex(Index) >= 0;
585}
586
587bool CEditor::IsQuadCornerSelected(int Index) const
588{
589 return m_SelectedQuadPoints & (1 << Index);
590}
591
592bool CEditor::IsQuadPointSelected(int QuadIndex, int Index) const
593{
594 return IsQuadSelected(Index: QuadIndex) && IsQuadCornerSelected(Index);
595}
596
597int CEditor::FindSelectedQuadIndex(int Index) const
598{
599 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
600 if(m_vSelectedQuads[i] == Index)
601 return i;
602 return -1;
603}
604
605int CEditor::FindEnvPointIndex(int Index, int Channel) const
606{
607 auto Iter = std::find(
608 first: m_vSelectedEnvelopePoints.begin(),
609 last: m_vSelectedEnvelopePoints.end(),
610 val: std::pair(Index, Channel));
611
612 if(Iter != m_vSelectedEnvelopePoints.end())
613 return Iter - m_vSelectedEnvelopePoints.begin();
614 else
615 return -1;
616}
617
618void CEditor::SelectEnvPoint(int Index)
619{
620 m_vSelectedEnvelopePoints.clear();
621
622 for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++)
623 m_vSelectedEnvelopePoints.emplace_back(args&: Index, args&: c);
624}
625
626void CEditor::SelectEnvPoint(int Index, int Channel)
627{
628 DeselectEnvPoints();
629 m_vSelectedEnvelopePoints.emplace_back(args&: Index, args&: Channel);
630}
631
632void CEditor::ToggleEnvPoint(int Index, int Channel)
633{
634 if(IsTangentSelected())
635 DeselectEnvPoints();
636
637 int ListIndex = FindEnvPointIndex(Index, Channel);
638
639 if(ListIndex >= 0)
640 {
641 m_vSelectedEnvelopePoints.erase(position: m_vSelectedEnvelopePoints.begin() + ListIndex);
642 }
643 else
644 m_vSelectedEnvelopePoints.emplace_back(args&: Index, args&: Channel);
645}
646
647bool CEditor::IsEnvPointSelected(int Index, int Channel) const
648{
649 int ListIndex = FindEnvPointIndex(Index, Channel);
650
651 return ListIndex >= 0;
652}
653
654bool CEditor::IsEnvPointSelected(int Index) const
655{
656 auto Iter = std::find_if(
657 first: m_vSelectedEnvelopePoints.begin(),
658 last: m_vSelectedEnvelopePoints.end(),
659 pred: [&](auto pair) { return pair.first == Index; });
660
661 return Iter != m_vSelectedEnvelopePoints.end();
662}
663
664void CEditor::DeselectEnvPoints()
665{
666 m_vSelectedEnvelopePoints.clear();
667 m_SelectedTangentInPoint = std::pair(-1, -1);
668 m_SelectedTangentOutPoint = std::pair(-1, -1);
669}
670
671void CEditor::SelectTangentOutPoint(int Index, int Channel)
672{
673 DeselectEnvPoints();
674 m_SelectedTangentOutPoint = std::pair(Index, Channel);
675}
676
677bool CEditor::IsTangentOutPointSelected(int Index, int Channel) const
678{
679 return m_SelectedTangentOutPoint == std::pair(Index, Channel);
680}
681
682void CEditor::SelectTangentInPoint(int Index, int Channel)
683{
684 DeselectEnvPoints();
685 m_SelectedTangentInPoint = std::pair(Index, Channel);
686}
687
688bool CEditor::IsTangentInPointSelected(int Index, int Channel) const
689{
690 return m_SelectedTangentInPoint == std::pair(Index, Channel);
691}
692
693bool CEditor::IsTangentInSelected() const
694{
695 return m_SelectedTangentInPoint != std::pair(-1, -1);
696}
697
698bool CEditor::IsTangentOutSelected() const
699{
700 return m_SelectedTangentOutPoint != std::pair(-1, -1);
701}
702
703bool CEditor::IsTangentSelected() const
704{
705 return IsTangentInSelected() || IsTangentOutSelected();
706}
707
708std::pair<int, int> CEditor::EnvGetSelectedTimeAndValue() const
709{
710 if(m_SelectedEnvelope < 0 || m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
711 return {};
712
713 std::shared_ptr<CEnvelope> pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
714 int CurrentTime;
715 int CurrentValue;
716 if(IsTangentInSelected())
717 {
718 auto [SelectedIndex, SelectedChannel] = m_SelectedTangentInPoint;
719
720 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel];
721 CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel];
722 }
723 else if(IsTangentOutSelected())
724 {
725 auto [SelectedIndex, SelectedChannel] = m_SelectedTangentOutPoint;
726
727 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel];
728 CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel];
729 }
730 else
731 {
732 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints.front();
733
734 CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time;
735 CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel];
736 }
737
738 return std::pair<int, int>{CurrentTime, CurrentValue};
739}
740
741bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUser)
742{
743 CEditor *pEditor = (CEditor *)pUser;
744 if(pEditor->Load(pFilename: pFileName, StorageType))
745 {
746 pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder || (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentLink && str_comp(a: pEditor->m_aFileDialogCurrentLink, b: "themes") == 0));
747 if(pEditor->m_Dialog == DIALOG_FILE)
748 pEditor->m_Dialog = DIALOG_NONE;
749 return true;
750 }
751 else
752 {
753 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFileName);
754 return false;
755 }
756}
757
758bool CEditor::CallbackAppendMap(const char *pFileName, int StorageType, void *pUser)
759{
760 CEditor *pEditor = (CEditor *)pUser;
761 if(pEditor->Append(pFilename: pFileName, StorageType))
762 {
763 pEditor->m_Dialog = DIALOG_NONE;
764 return true;
765 }
766 else
767 {
768 pEditor->m_aFileName[0] = 0;
769 pEditor->ShowFileDialogError(pFormat: "Failed to load map from file '%s'.", pFileName);
770 return false;
771 }
772}
773
774bool CEditor::CallbackSaveMap(const char *pFileName, int StorageType, void *pUser)
775{
776 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
777
778 CEditor *pEditor = static_cast<CEditor *>(pUser);
779 char aBuf[IO_MAX_PATH_LENGTH];
780 // add map extension
781 if(!str_endswith(str: pFileName, suffix: ".map"))
782 {
783 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.map", pFileName);
784 pFileName = aBuf;
785 }
786
787 // Save map to specified file
788 if(pEditor->Save(pFilename: pFileName))
789 {
790 str_copy(dst&: pEditor->m_aFileName, src: pFileName);
791 pEditor->m_ValidSaveFilename = true;
792 pEditor->m_Map.m_Modified = false;
793 }
794 else
795 {
796 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFileName);
797 return false;
798 }
799
800 // Also update autosave if it's older than half the configured autosave interval, so we also have periodic backups.
801 const float Time = pEditor->Client()->GlobalTime();
802 if(g_Config.m_EdAutosaveInterval > 0 && pEditor->m_Map.m_LastSaveTime < Time && Time - pEditor->m_Map.m_LastSaveTime > 30 * g_Config.m_EdAutosaveInterval)
803 {
804 if(!pEditor->PerformAutosave())
805 return false;
806 }
807
808 pEditor->m_Dialog = DIALOG_NONE;
809 return true;
810}
811
812bool CEditor::CallbackSaveCopyMap(const char *pFileName, int StorageType, void *pUser)
813{
814 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
815
816 CEditor *pEditor = static_cast<CEditor *>(pUser);
817 char aBuf[IO_MAX_PATH_LENGTH];
818 // add map extension
819 if(!str_endswith(str: pFileName, suffix: ".map"))
820 {
821 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.map", pFileName);
822 pFileName = aBuf;
823 }
824
825 if(pEditor->Save(pFilename: pFileName))
826 {
827 pEditor->m_Dialog = DIALOG_NONE;
828 return true;
829 }
830 else
831 {
832 pEditor->ShowFileDialogError(pFormat: "Failed to save map to file '%s'.", pFileName);
833 return false;
834 }
835}
836
837bool CEditor::CallbackSaveImage(const char *pFileName, int StorageType, void *pUser)
838{
839 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
840
841 CEditor *pEditor = static_cast<CEditor *>(pUser);
842 char aBuf[IO_MAX_PATH_LENGTH];
843
844 // add file extension
845 if(!str_endswith(str: pFileName, suffix: ".png"))
846 {
847 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.png", pFileName);
848 pFileName = aBuf;
849 }
850
851 std::shared_ptr<CEditorImage> pImg = pEditor->m_Map.m_vpImages[pEditor->m_SelectedImage];
852
853 EImageFormat OutputFormat;
854 switch(pImg->m_Format)
855 {
856 case CImageInfo::FORMAT_RGB:
857 OutputFormat = IMAGE_FORMAT_RGB;
858 break;
859 case CImageInfo::FORMAT_RGBA:
860 OutputFormat = IMAGE_FORMAT_RGBA;
861 break;
862 case CImageInfo::FORMAT_SINGLE_COMPONENT:
863 OutputFormat = IMAGE_FORMAT_R;
864 break;
865 default:
866 dbg_assert(false, "Image has invalid format.");
867 return false;
868 };
869
870 TImageByteBuffer ByteBuffer;
871 SImageByteBuffer ImageByteBuffer(&ByteBuffer);
872 if(SavePng(ImageFormat: OutputFormat, pRawBuffer: pImg->m_pData, WrittenBytes&: ImageByteBuffer, Width: pImg->m_Width, Height: pImg->m_Height))
873 {
874 IOHANDLE File = pEditor->Storage()->OpenFile(pFilename: pFileName, Flags: IOFLAG_WRITE, Type: StorageType);
875 if(File)
876 {
877 io_write(io: File, buffer: &ByteBuffer.front(), size: ByteBuffer.size());
878 io_close(io: File);
879 pEditor->m_Dialog = DIALOG_NONE;
880 return true;
881 }
882 pEditor->ShowFileDialogError(pFormat: "Failed to open file '%s'.", pFileName);
883 return false;
884 }
885 else
886 {
887 pEditor->ShowFileDialogError(pFormat: "Failed to write image to file.");
888 return false;
889 }
890}
891
892bool CEditor::CallbackSaveSound(const char *pFileName, int StorageType, void *pUser)
893{
894 dbg_assert(StorageType == IStorage::TYPE_SAVE, "Saving only allowed for IStorage::TYPE_SAVE");
895
896 CEditor *pEditor = static_cast<CEditor *>(pUser);
897 char aBuf[IO_MAX_PATH_LENGTH];
898
899 // add file extension
900 if(!str_endswith(str: pFileName, suffix: ".opus"))
901 {
902 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s.opus", pFileName);
903 pFileName = aBuf;
904 }
905 std::shared_ptr<CEditorSound> pSound = pEditor->m_Map.m_vpSounds[pEditor->m_SelectedSound];
906 IOHANDLE File = pEditor->Storage()->OpenFile(pFilename: pFileName, Flags: IOFLAG_WRITE, Type: StorageType);
907
908 if(File)
909 {
910 io_write(io: File, buffer: pSound->m_pData, size: pSound->m_DataSize);
911 io_close(io: File);
912 pEditor->OnDialogClose();
913 pEditor->m_Dialog = DIALOG_NONE;
914 return true;
915 }
916 pEditor->ShowFileDialogError(pFormat: "Failed to open file '%s'.", pFileName);
917 return false;
918}
919
920void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonId, const void *pStopButtonId, const void *pSeekBarId, const int SampleId)
921{
922 CUIRect Button, SeekBar;
923 // play/pause button
924 {
925 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
926 if(DoButton_FontIcon(pId: pPlayPauseButtonId, pText: Sound()->IsPlaying(SampleId) ? FONT_ICON_PAUSE : FONT_ICON_PLAY, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Play/pause audio preview", Corners: IGraphics::CORNER_ALL) ||
927 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_SPACE)))
928 {
929 if(Sound()->IsPlaying(SampleId))
930 {
931 Sound()->Pause(SampleId);
932 }
933 else
934 {
935 if(SampleId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
936 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
937
938 Sound()->Play(ChannelId: CSounds::CHN_GUI, SampleId, Flags: ISound::FLAG_PREVIEW);
939 }
940 }
941 }
942 // stop button
943 {
944 View.VSplitLeft(Cut: 2.0f, pLeft: nullptr, pRight: &View);
945 View.VSplitLeft(Cut: View.h, pLeft: &Button, pRight: &View);
946 if(DoButton_FontIcon(pId: pStopButtonId, pText: FONT_ICON_STOP, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Stop audio preview", Corners: IGraphics::CORNER_ALL))
947 {
948 Sound()->Stop(SampleId);
949 }
950 }
951 // do seekbar
952 {
953 View.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &View);
954 const float Cut = std::min(a: View.w, b: 200.0f);
955 View.VSplitLeft(Cut, pLeft: &SeekBar, pRight: &View);
956 SeekBar.HMargin(Cut: 2.5f, pOtherRect: &SeekBar);
957
958 const float Rounding = 5.0f;
959
960 char aBuffer[64];
961 const float CurrentTime = Sound()->GetSampleCurrentTime(SampleId);
962 const float TotalTime = Sound()->GetSampleTotalTime(SampleId);
963
964 // draw seek bar
965 SeekBar.Draw(Color: ColorRGBA(0, 0, 0, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
966
967 // draw filled bar
968 const float Amount = CurrentTime / TotalTime;
969 CUIRect FilledBar = SeekBar;
970 FilledBar.w = 2 * Rounding + (FilledBar.w - 2 * Rounding) * Amount;
971 FilledBar.Draw(Color: ColorRGBA(1, 1, 1, 0.5f), Corners: IGraphics::CORNER_ALL, Rounding);
972
973 // draw time
974 char aCurrentTime[32];
975 str_time_float(secs: CurrentTime, format: TIME_HOURS, buffer: aCurrentTime, buffer_size: sizeof(aCurrentTime));
976 char aTotalTime[32];
977 str_time_float(secs: TotalTime, format: TIME_HOURS, buffer: aTotalTime, buffer_size: sizeof(aTotalTime));
978 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s / %s", aCurrentTime, aTotalTime);
979 Ui()->DoLabel(pRect: &SeekBar, pText: aBuffer, Size: SeekBar.h * 0.70f, Align: TEXTALIGN_MC);
980
981 // do the logic
982 const bool Inside = Ui()->MouseInside(pRect: &SeekBar);
983
984 if(Ui()->CheckActiveItem(pId: pSeekBarId))
985 {
986 if(!Ui()->MouseButton(Index: 0))
987 {
988 Ui()->SetActiveItem(nullptr);
989 }
990 else
991 {
992 const float AmountSeek = clamp(val: (Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), lo: 0.0f, hi: 1.0f);
993 Sound()->SetSampleCurrentTime(SampleId, Time: AmountSeek);
994 }
995 }
996 else if(Ui()->HotItem() == pSeekBarId)
997 {
998 if(Ui()->MouseButton(Index: 0))
999 Ui()->SetActiveItem(pSeekBarId);
1000 }
1001
1002 if(Inside && !Ui()->MouseButton(Index: 0))
1003 Ui()->SetHotItem(pSeekBarId);
1004 }
1005}
1006
1007void CEditor::DoToolbarLayers(CUIRect ToolBar)
1008{
1009 const bool ModPressed = Input()->ModifierIsPressed();
1010 const bool ShiftPressed = Input()->ShiftIsPressed();
1011
1012 // handle shortcut for info button
1013 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && !ShiftPressed)
1014 {
1015 if(m_ShowTileInfo == SHOW_TILE_HEXADECIMAL)
1016 m_ShowTileInfo = SHOW_TILE_DECIMAL;
1017 else if(m_ShowTileInfo != SHOW_TILE_OFF)
1018 m_ShowTileInfo = SHOW_TILE_OFF;
1019 else
1020 m_ShowTileInfo = SHOW_TILE_DECIMAL;
1021 m_ShowEnvelopePreview = SHOWENV_NONE;
1022 }
1023
1024 // handle shortcut for hex button
1025 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_I) && ModPressed && ShiftPressed)
1026 {
1027 m_ShowTileInfo = m_ShowTileInfo == SHOW_TILE_HEXADECIMAL ? SHOW_TILE_OFF : SHOW_TILE_HEXADECIMAL;
1028 m_ShowEnvelopePreview = SHOWENV_NONE;
1029 }
1030
1031 // handle shortcut for unused button
1032 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_U) && ModPressed)
1033 m_AllowPlaceUnusedTiles = !m_AllowPlaceUnusedTiles;
1034
1035 CUIRect TB_Top, TB_Bottom;
1036 CUIRect Button;
1037
1038 ToolBar.HSplitMid(pTop: &TB_Top, pBottom: &TB_Bottom, Spacing: 5.0f);
1039
1040 // top line buttons
1041 {
1042 // detail button
1043 TB_Top.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &TB_Top);
1044 static int s_HqButton = 0;
1045 if(DoButton_Editor(pId: &s_HqButton, pText: "HD", Checked: m_ShowDetail, pRect: &Button, Flags: 0, pToolTip: "[ctrl+h] Toggle High Detail") ||
1046 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_H) && ModPressed))
1047 {
1048 m_ShowDetail = !m_ShowDetail;
1049 }
1050
1051 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1052
1053 // animation button
1054 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1055 static char s_AnimateButton;
1056 if(DoButton_FontIcon(pId: &s_AnimateButton, pText: FONT_ICON_CIRCLE_PLAY, Checked: m_Animate, pRect: &Button, Flags: 0, pToolTip: "[ctrl+m] Toggle animation", Corners: IGraphics::CORNER_L) ||
1057 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_M) && ModPressed))
1058 {
1059 m_AnimateStart = time_get();
1060 m_Animate = !m_Animate;
1061 }
1062
1063 // animation settings button
1064 TB_Top.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &TB_Top);
1065 static char s_AnimateSettingsButton;
1066 if(DoButton_FontIcon(pId: &s_AnimateSettingsButton, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Change animation settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
1067 {
1068 m_AnimateUpdatePopup = true;
1069 static SPopupMenuId s_PopupAnimateSettingsId;
1070 Ui()->DoPopupMenu(pId: &s_PopupAnimateSettingsId, X: Button.x, Y: Button.y + Button.h, Width: 150.0f, Height: 37.0f, pContext: this, pfnFunc: PopupAnimateSettings);
1071 }
1072
1073 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1074
1075 // proof button
1076 TB_Top.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &TB_Top);
1077 static int s_ProofButton = 0;
1078 if(DoButton_Ex(pId: &s_ProofButton, pText: "Proof", Checked: MapView()->ProofMode()->IsEnabled(), pRect: &Button, Flags: 0, pToolTip: "[ctrl+p] Toggles proof borders. These borders represent what a player maximum can see.", Corners: IGraphics::CORNER_L) ||
1079 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_P) && ModPressed))
1080 {
1081 MapView()->ProofMode()->Toggle();
1082 }
1083
1084 TB_Top.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &TB_Top);
1085 static int s_ProofModeButton = 0;
1086 if(DoButton_FontIcon(pId: &s_ProofModeButton, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Select proof mode.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
1087 {
1088 static SPopupMenuId s_PopupProofModeId;
1089 Ui()->DoPopupMenu(pId: &s_PopupProofModeId, X: Button.x, Y: Button.y + Button.h, Width: 60.0f, Height: 36.0f, pContext: this, pfnFunc: PopupProofMode);
1090 }
1091
1092 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1093
1094 // zoom button
1095 TB_Top.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &TB_Top);
1096 static int s_ZoomButton = 0;
1097 if(DoButton_Editor(pId: &s_ZoomButton, pText: "Zoom", Checked: m_PreviewZoom, pRect: &Button, Flags: 0, pToolTip: "Toggles preview of how layers will be zoomed in-game"))
1098 {
1099 m_PreviewZoom = !m_PreviewZoom;
1100 }
1101
1102 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1103
1104 // grid button
1105 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1106 static int s_GridButton = 0;
1107 if(DoButton_FontIcon(pId: &s_GridButton, pText: FONT_ICON_BORDER_ALL, Checked: MapView()->MapGrid()->IsEnabled(), pRect: &Button, Flags: 0, pToolTip: "[ctrl+g] Toggle Grid", Corners: IGraphics::CORNER_L) ||
1108 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_G) && ModPressed && !ShiftPressed))
1109 {
1110 MapView()->MapGrid()->Toggle();
1111 }
1112
1113 // grid settings button
1114 TB_Top.VSplitLeft(Cut: 14.0f, pLeft: &Button, pRight: &TB_Top);
1115 static char s_GridSettingsButton;
1116 if(DoButton_FontIcon(pId: &s_GridSettingsButton, pText: FONT_ICON_CIRCLE_CHEVRON_DOWN, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Change grid settings.", Corners: IGraphics::CORNER_R, FontSize: 8.0f))
1117 {
1118 MapView()->MapGrid()->DoSettingsPopup(Position: vec2(Button.x, Button.y + Button.h));
1119 }
1120
1121 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1122
1123 // zoom group
1124 TB_Top.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &TB_Top);
1125 static int s_ZoomOutButton = 0;
1126 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FONT_ICON_MINUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad-] Zoom out", Corners: IGraphics::CORNER_L))
1127 {
1128 MapView()->Zoom()->ChangeValue(Amount: 50.0f);
1129 }
1130
1131 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1132 static int s_ZoomNormalButton = 0;
1133 if(DoButton_FontIcon(pId: &s_ZoomNormalButton, pText: FONT_ICON_MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad*] Zoom to normal and remove editor offset", Corners: IGraphics::CORNER_NONE))
1134 {
1135 MapView()->ResetZoom();
1136 }
1137
1138 TB_Top.VSplitLeft(Cut: 20.0f, pLeft: &Button, pRight: &TB_Top);
1139 static int s_ZoomInButton = 0;
1140 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FONT_ICON_PLUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad+] Zoom in", Corners: IGraphics::CORNER_R))
1141 {
1142 MapView()->Zoom()->ChangeValue(Amount: -50.0f);
1143 }
1144
1145 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1146
1147 // undo/redo group
1148 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1149 static int s_UndoButton = 0;
1150 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FONT_ICON_UNDO, Checked: m_EditorHistory.CanUndo() - 1, pRect: &Button, Flags: 0, pToolTip: "[ctrl+z] Undo last action", Corners: IGraphics::CORNER_L))
1151 {
1152 m_EditorHistory.Undo();
1153 }
1154
1155 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1156 static int s_RedoButton = 0;
1157 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FONT_ICON_REDO, Checked: m_EditorHistory.CanRedo() - 1, pRect: &Button, Flags: 0, pToolTip: "[ctrl+y] Redo last action", Corners: IGraphics::CORNER_R))
1158 {
1159 m_EditorHistory.Redo();
1160 }
1161
1162 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1163
1164 // brush manipulation
1165 {
1166 int Enabled = m_pBrush->IsEmpty() ? -1 : 0;
1167
1168 // flip buttons
1169 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1170 static int s_FlipXButton = 0;
1171 if(DoButton_FontIcon(pId: &s_FlipXButton, pText: FONT_ICON_ARROWS_LEFT_RIGHT, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[N] Flip brush horizontal", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_N) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1172 {
1173 for(auto &pLayer : m_pBrush->m_vpLayers)
1174 pLayer->BrushFlipX();
1175 }
1176
1177 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1178 static int s_FlipyButton = 0;
1179 if(DoButton_FontIcon(pId: &s_FlipyButton, pText: FONT_ICON_ARROWS_UP_DOWN, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[M] Flip brush vertical", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_M) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1180 {
1181 for(auto &pLayer : m_pBrush->m_vpLayers)
1182 pLayer->BrushFlipY();
1183 }
1184 TB_Top.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Top);
1185
1186 // rotate buttons
1187 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1188 static int s_RotationAmount = 90;
1189 bool TileLayer = false;
1190 // check for tile layers in brush selection
1191 for(auto &pLayer : m_pBrush->m_vpLayers)
1192 if(pLayer->m_Type == LAYERTYPE_TILES)
1193 {
1194 TileLayer = true;
1195 s_RotationAmount = maximum(a: 90, b: (s_RotationAmount / 90) * 90);
1196 break;
1197 }
1198
1199 static int s_CcwButton = 0;
1200 if(DoButton_FontIcon(pId: &s_CcwButton, pText: FONT_ICON_ARROW_ROTATE_LEFT, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[R] Rotates the brush counter clockwise", Corners: IGraphics::CORNER_L) || (Input()->KeyPress(Key: KEY_R) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1201 {
1202 for(auto &pLayer : m_pBrush->m_vpLayers)
1203 pLayer->BrushRotate(Amount: -s_RotationAmount / 360.0f * pi * 2);
1204 }
1205
1206 TB_Top.VSplitLeft(Cut: 30.0f, pLeft: &Button, pRight: &TB_Top);
1207 auto RotationAmountRes = UiDoValueSelector(pId: &s_RotationAmount, pRect: &Button, pLabel: "", Current: s_RotationAmount, Min: TileLayer ? 90 : 1, Max: 359, Step: TileLayer ? 90 : 1, Scale: TileLayer ? 10.0f : 2.0f, pToolTip: "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", IsDegree: true, IsHex: false, Corners: IGraphics::CORNER_NONE);
1208 s_RotationAmount = RotationAmountRes.m_Value;
1209
1210 TB_Top.VSplitLeft(Cut: 25.0f, pLeft: &Button, pRight: &TB_Top);
1211 static int s_CwButton = 0;
1212 if(DoButton_FontIcon(pId: &s_CwButton, pText: FONT_ICON_ARROW_ROTATE_RIGHT, Checked: Enabled, pRect: &Button, Flags: 0, pToolTip: "[T] Rotates the brush clockwise", Corners: IGraphics::CORNER_R) || (Input()->KeyPress(Key: KEY_T) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
1213 {
1214 for(auto &pLayer : m_pBrush->m_vpLayers)
1215 pLayer->BrushRotate(Amount: s_RotationAmount / 360.0f * pi * 2);
1216 }
1217 }
1218
1219 // Color pipette and palette
1220 {
1221 const float PipetteButtonWidth = 30.0f;
1222 const float ColorPickerButtonWidth = 20.0f;
1223 const float Spacing = 2.0f;
1224 const size_t NumColorsShown = clamp<int>(val: round_to_int(f: (TB_Top.w - PipetteButtonWidth - 40.0f) / (ColorPickerButtonWidth + Spacing)), lo: 1, hi: std::size(m_aSavedColors));
1225
1226 CUIRect ColorPalette;
1227 TB_Top.VSplitRight(Cut: NumColorsShown * (ColorPickerButtonWidth + Spacing) + PipetteButtonWidth, pLeft: &TB_Top, pRight: &ColorPalette);
1228
1229 // Pipette button
1230 static char s_PipetteButton;
1231 ColorPalette.VSplitLeft(Cut: PipetteButtonWidth, pLeft: &Button, pRight: &ColorPalette);
1232 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
1233 if(DoButton_FontIcon(pId: &s_PipetteButton, pText: FONT_ICON_EYE_DROPPER, Checked: m_ColorPipetteActive ? 1 : 0, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Shift+C] Color pipette. Pick a color from the screen by clicking on it.", Corners: IGraphics::CORNER_ALL) ||
1234 (CLineInput::GetActiveInput() == nullptr && ModPressed && ShiftPressed && Input()->KeyPress(Key: KEY_C)))
1235 {
1236 m_ColorPipetteActive = !m_ColorPipetteActive;
1237 }
1238
1239 // Palette color pickers
1240 for(size_t i = 0; i < NumColorsShown; ++i)
1241 {
1242 ColorPalette.VSplitLeft(Cut: ColorPickerButtonWidth, pLeft: &Button, pRight: &ColorPalette);
1243 ColorPalette.VSplitLeft(Cut: Spacing, pLeft: nullptr, pRight: &ColorPalette);
1244 const auto &&SetColor = [&](ColorRGBA NewColor) {
1245 m_aSavedColors[i] = NewColor;
1246 };
1247 DoColorPickerButton(pId: &m_aSavedColors[i], pRect: &Button, Color: m_aSavedColors[i], SetColor);
1248 }
1249 }
1250 }
1251
1252 // Bottom line buttons
1253 {
1254 // refocus button
1255 {
1256 TB_Bottom.VSplitLeft(Cut: 50.0f, pLeft: &Button, pRight: &TB_Bottom);
1257 static int s_RefocusButton = 0;
1258 int FocusButtonChecked = MapView()->IsFocused() ? -1 : 1;
1259 if(DoButton_Editor(pId: &s_RefocusButton, pText: "Refocus", Checked: FocusButtonChecked, pRect: &Button, Flags: 0, pToolTip: "[HOME] Restore map focus") || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_HOME)))
1260 MapView()->Focus();
1261 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Bottom);
1262 }
1263
1264 // tile manipulation
1265 {
1266 // do tele/tune/switch/speedup button
1267 {
1268 std::shared_ptr<CLayerTiles> pS = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
1269 if(pS)
1270 {
1271 const char *pButtonName = nullptr;
1272 CUi::FPopupMenuFunction pfnPopupFunc = nullptr;
1273 int Rows = 0;
1274 if(pS == m_Map.m_pSwitchLayer)
1275 {
1276 pButtonName = "Switch";
1277 pfnPopupFunc = PopupSwitch;
1278 Rows = 3;
1279 }
1280 else if(pS == m_Map.m_pSpeedupLayer)
1281 {
1282 pButtonName = "Speedup";
1283 pfnPopupFunc = PopupSpeedup;
1284 Rows = 3;
1285 }
1286 else if(pS == m_Map.m_pTuneLayer)
1287 {
1288 pButtonName = "Tune";
1289 pfnPopupFunc = PopupTune;
1290 Rows = 1;
1291 }
1292 else if(pS == m_Map.m_pTeleLayer)
1293 {
1294 pButtonName = "Tele";
1295 pfnPopupFunc = PopupTele;
1296 Rows = 3;
1297 }
1298
1299 if(pButtonName != nullptr)
1300 {
1301 static char s_aButtonTooltip[64];
1302 str_format(buffer: s_aButtonTooltip, buffer_size: sizeof(s_aButtonTooltip), format: "[ctrl+t] %s", pButtonName);
1303
1304 TB_Bottom.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &TB_Bottom);
1305 static int s_ModifierButton = 0;
1306 if(DoButton_Ex(pId: &s_ModifierButton, pText: pButtonName, Checked: 0, pRect: &Button, Flags: 0, pToolTip: s_aButtonTooltip, Corners: IGraphics::CORNER_ALL) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && ModPressed && Input()->KeyPress(Key: KEY_T)))
1307 {
1308 static SPopupMenuId s_PopupModifierId;
1309 if(!Ui()->IsPopupOpen(pId: &s_PopupModifierId))
1310 {
1311 Ui()->DoPopupMenu(pId: &s_PopupModifierId, X: Button.x, Y: Button.y + Button.h, Width: 120, Height: 10.0f + Rows * 13.0f, pContext: this, pfnFunc: pfnPopupFunc);
1312 }
1313 }
1314 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &TB_Bottom);
1315 }
1316 }
1317 }
1318 }
1319
1320 // do add quad/sound button
1321 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index: 0);
1322 if(pLayer && (pLayer->m_Type == LAYERTYPE_QUADS || pLayer->m_Type == LAYERTYPE_SOUNDS))
1323 {
1324 TB_Bottom.VSplitLeft(Cut: 60.0f, pLeft: &Button, pRight: &TB_Bottom);
1325
1326 bool Invoked = false;
1327 static int s_AddItemButton = 0;
1328
1329 if(pLayer->m_Type == LAYERTYPE_QUADS)
1330 {
1331 Invoked = DoButton_Editor(pId: &s_AddItemButton, pText: "Add Quad", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[ctrl+q] Add a new quad") ||
1332 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed);
1333 }
1334 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
1335 {
1336 Invoked = DoButton_Editor(pId: &s_AddItemButton, pText: "Add Sound", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[ctrl+q] Add a new sound source") ||
1337 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed);
1338 }
1339
1340 if(Invoked)
1341 {
1342 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
1343
1344 float aMapping[4];
1345 pGroup->Mapping(pPoints: aMapping);
1346 int x = aMapping[0] + (aMapping[2] - aMapping[0]) / 2;
1347 int y = aMapping[1] + (aMapping[3] - aMapping[1]) / 2;
1348 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_Q) && ModPressed)
1349 {
1350 x += Ui()->MouseWorldX() - (MapView()->GetWorldOffset().x * pGroup->m_ParallaxX / 100) - pGroup->m_OffsetX;
1351 y += Ui()->MouseWorldY() - (MapView()->GetWorldOffset().y * pGroup->m_ParallaxY / 100) - pGroup->m_OffsetY;
1352 }
1353
1354 if(pLayer->m_Type == LAYERTYPE_QUADS)
1355 m_EditorHistory.Execute(pAction: std::make_shared<CEditorActionNewEmptyQuad>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0], args&: x, args&: y));
1356 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
1357 m_EditorHistory.Execute(pAction: std::make_shared<CEditorActionNewEmptySound>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0], args&: x, args&: y));
1358 }
1359 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &TB_Bottom);
1360 }
1361
1362 // Brush draw mode button
1363 {
1364 TB_Bottom.VSplitLeft(Cut: 65.0f, pLeft: &Button, pRight: &TB_Bottom);
1365 static int s_BrushDrawModeButton = 0;
1366 if(DoButton_Editor(pId: &s_BrushDrawModeButton, pText: "Destructive", Checked: m_BrushDrawDestructive, pRect: &Button, Flags: 0, pToolTip: "[ctrl+d] Toggle brush draw mode") ||
1367 (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_D) && ModPressed && !ShiftPressed))
1368 m_BrushDrawDestructive = !m_BrushDrawDestructive;
1369 TB_Bottom.VSplitLeft(Cut: 5.0f, pLeft: &Button, pRight: &TB_Bottom);
1370 }
1371 }
1372}
1373
1374void CEditor::DoToolbarSounds(CUIRect ToolBar)
1375{
1376 CUIRect ToolBarTop, ToolBarBottom;
1377 ToolBar.HSplitMid(pTop: &ToolBarTop, pBottom: &ToolBarBottom, Spacing: 5.0f);
1378
1379 if(m_SelectedSound >= 0 && (size_t)m_SelectedSound < m_Map.m_vpSounds.size())
1380 {
1381 const std::shared_ptr<CEditorSound> pSelectedSound = m_Map.m_vpSounds[m_SelectedSound];
1382 if(pSelectedSound->m_SoundId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
1383 Sound()->Stop(SampleId: m_ToolbarPreviewSound);
1384 m_ToolbarPreviewSound = pSelectedSound->m_SoundId;
1385 }
1386 else
1387 {
1388 m_ToolbarPreviewSound = -1;
1389 }
1390
1391 if(m_ToolbarPreviewSound >= 0)
1392 {
1393 static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0;
1394 DoAudioPreview(View: ToolBarBottom, pPlayPauseButtonId: &s_PlayPauseButton, pStopButtonId: &s_StopButton, pSeekBarId: &s_SeekBar, SampleId: m_ToolbarPreviewSound);
1395 }
1396}
1397
1398static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation)
1399{
1400 int x = pPoint->x - pCenter->x;
1401 int y = pPoint->y - pCenter->y;
1402 pPoint->x = (int)(x * std::cos(x: Rotation) - y * std::sin(x: Rotation) + pCenter->x);
1403 pPoint->y = (int)(x * std::sin(x: Rotation) + y * std::cos(x: Rotation) + pCenter->y);
1404}
1405
1406void CEditor::DoSoundSource(int LayerIndex, CSoundSource *pSource, int Index)
1407{
1408 void *pId = &pSource->m_Position;
1409
1410 static ESoundSourceOp s_Operation = ESoundSourceOp::OP_NONE;
1411
1412 float wx = Ui()->MouseWorldX();
1413 float wy = Ui()->MouseWorldY();
1414
1415 float CenterX = fx2f(v: pSource->m_Position.x);
1416 float CenterY = fx2f(v: pSource->m_Position.y);
1417
1418 float dx = (CenterX - wx) / m_MouseWorldScale;
1419 float dy = (CenterY - wy) / m_MouseWorldScale;
1420 if(dx * dx + dy * dy < 50)
1421 Ui()->SetHotItem(pId);
1422
1423 const bool IgnoreGrid = Input()->AltIsPressed();
1424 static CSoundSourceOperationTracker s_Tracker(this);
1425
1426 if(s_Operation == ESoundSourceOp::OP_NONE)
1427 {
1428 if(!Ui()->MouseButton(Index: 0))
1429 s_Tracker.End();
1430 }
1431
1432 if(Ui()->CheckActiveItem(pId))
1433 {
1434 if(s_Operation != ESoundSourceOp::OP_NONE)
1435 {
1436 s_Tracker.Begin(pSource, Operation: s_Operation, LayerIndex);
1437 }
1438
1439 if(m_MouseDeltaWorld != vec2(0.0f, 0.0f))
1440 {
1441 if(s_Operation == ESoundSourceOp::OP_MOVE)
1442 {
1443 float x = wx;
1444 float y = wy;
1445 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
1446 MapView()->MapGrid()->SnapToGrid(x, y);
1447 pSource->m_Position.x = f2fx(v: x);
1448 pSource->m_Position.y = f2fx(v: y);
1449 }
1450 }
1451
1452 if(s_Operation == ESoundSourceOp::OP_CONTEXT_MENU)
1453 {
1454 if(!Ui()->MouseButton(Index: 1))
1455 {
1456 if(m_vSelectedLayers.size() == 1)
1457 {
1458 static SPopupMenuId s_PopupSourceId;
1459 Ui()->DoPopupMenu(pId: &s_PopupSourceId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 200, pContext: this, pfnFunc: PopupSource);
1460 Ui()->DisableMouseLock();
1461 }
1462 s_Operation = ESoundSourceOp::OP_NONE;
1463 Ui()->SetActiveItem(nullptr);
1464 }
1465 }
1466 else
1467 {
1468 if(!Ui()->MouseButton(Index: 0))
1469 {
1470 Ui()->DisableMouseLock();
1471 s_Operation = ESoundSourceOp::OP_NONE;
1472 Ui()->SetActiveItem(nullptr);
1473 }
1474 }
1475
1476 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1477 }
1478 else if(Ui()->HotItem() == pId)
1479 {
1480 ms_pUiGotContext = pId;
1481
1482 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
1483 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold alt to ignore grid.");
1484
1485 if(Ui()->MouseButton(Index: 0))
1486 {
1487 s_Operation = ESoundSourceOp::OP_MOVE;
1488
1489 Ui()->SetActiveItem(pId);
1490 m_SelectedSource = Index;
1491 }
1492
1493 if(Ui()->MouseButton(Index: 1))
1494 {
1495 m_SelectedSource = Index;
1496 s_Operation = ESoundSourceOp::OP_CONTEXT_MENU;
1497 Ui()->SetActiveItem(pId);
1498 }
1499 }
1500 else
1501 {
1502 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
1503 }
1504
1505 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale);
1506 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1507}
1508
1509void CEditor::PreparePointDrag(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex)
1510{
1511 m_QuadDragOriginalPoints[QuadIndex][PointIndex] = pQuad->m_aPoints[PointIndex];
1512}
1513
1514void CEditor::DoPointDrag(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY)
1515{
1516 pQuad->m_aPoints[PointIndex].x = m_QuadDragOriginalPoints[QuadIndex][PointIndex].x + OffsetX;
1517 pQuad->m_aPoints[PointIndex].y = m_QuadDragOriginalPoints[QuadIndex][PointIndex].y + OffsetY;
1518}
1519
1520CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) const
1521{
1522 if(Input()->ShiftIsPressed())
1523 if(absolute(a: OffsetX) < absolute(a: OffsetY))
1524 return EAxis::AXIS_Y;
1525 else
1526 return EAxis::AXIS_X;
1527 else
1528 return EAxis::AXIS_NONE;
1529}
1530
1531void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) const
1532{
1533 if(Axis == EAxis::AXIS_NONE)
1534 return;
1535
1536 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1537 if(Axis == EAxis::AXIS_X)
1538 {
1539 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x + Point.x) / 2.0f, fx2f(v: OriginalPoint.y), fx2f(v: Point.x - OriginalPoint.x), 1.0f * m_MouseWorldScale);
1540 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
1541 }
1542 else if(Axis == EAxis::AXIS_Y)
1543 {
1544 IGraphics::CQuadItem Line(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y + Point.y) / 2.0f, 1.0f * m_MouseWorldScale, fx2f(v: Point.y - OriginalPoint.y));
1545 Graphics()->QuadsDraw(pArray: &Line, Num: 1);
1546 }
1547
1548 // Draw ghost of original point
1549 IGraphics::CQuadItem QuadItem(fx2f(v: OriginalPoint.x), fx2f(v: OriginalPoint.y), 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale);
1550 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
1551}
1552
1553void CEditor::ComputePointAlignments(const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY, std::vector<SAlignmentInfo> &vAlignments, bool Append) const
1554{
1555 if(!Append)
1556 vAlignments.clear();
1557 if(!g_Config.m_EdAlignQuads)
1558 return;
1559
1560 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1561
1562 // Perform computation from the original position of this point
1563 int Threshold = f2fx(v: maximum(a: 5.0f, b: 10.0f * m_MouseWorldScale));
1564 CPoint OrigPoint = m_QuadDragOriginalPoints.at(k: QuadIndex)[PointIndex];
1565 // Get the "current" point by applying the offset
1566 CPoint Point = OrigPoint + ivec2(OffsetX, OffsetY);
1567
1568 // Save smallest diff on both axis to only keep closest alignments
1569 int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1;
1570 // Store both axis alignments in separate vectors
1571 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1572
1573 // Check if we can align/snap to a specific point
1574 auto &&CheckAlignment = [&](CPoint *pQuadPoint) {
1575 int DX = pQuadPoint->x - Point.x;
1576 int DY = pQuadPoint->y - Point.y;
1577 int DiffX = absolute(a: DX);
1578 int DiffY = absolute(a: DY);
1579
1580 if(DiffX <= Threshold && (!GridEnabled || DiffX == 0))
1581 {
1582 // Only store alignments that have the smallest difference
1583 if(DiffX < SmallestDiffX)
1584 {
1585 vAlignmentsX.clear();
1586 SmallestDiffX = DiffX;
1587 }
1588
1589 // We can have multiple alignments having the same difference/distance
1590 if(DiffX == SmallestDiffX)
1591 {
1592 vAlignmentsX.push_back(x: SAlignmentInfo{
1593 .m_AlignedPoint: *pQuadPoint, // Aligned point
1594 {.m_X: OrigPoint.y}, // Value that can change (which is not snapped), original position
1595 .m_Axis: EAxis::AXIS_Y, // The alignment axis
1596 .m_PointIndex: PointIndex, // The index of the point
1597 .m_Diff: DX,
1598 });
1599 }
1600 }
1601
1602 if(DiffY <= Threshold && (!GridEnabled || DiffY == 0))
1603 {
1604 // Only store alignments that have the smallest difference
1605 if(DiffY < SmallestDiffY)
1606 {
1607 vAlignmentsY.clear();
1608 SmallestDiffY = DiffY;
1609 }
1610
1611 if(DiffY == SmallestDiffY)
1612 {
1613 vAlignmentsY.push_back(x: SAlignmentInfo{
1614 .m_AlignedPoint: *pQuadPoint,
1615 {.m_X: OrigPoint.x},
1616 .m_Axis: EAxis::AXIS_X,
1617 .m_PointIndex: PointIndex,
1618 .m_Diff: DY,
1619 });
1620 }
1621 }
1622 };
1623
1624 // Iterate through all the quads of the current layer
1625 // Check alignment with each point of the quad (corners & pivot)
1626 // Compute an AABB (Axis Aligned Bounding Box) to get the center of the quad
1627 // Check alignment with the center of the quad
1628 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1629 {
1630 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1631 CPoint Min = pCurrentQuad->m_aPoints[0];
1632 CPoint Max = pCurrentQuad->m_aPoints[0];
1633
1634 for(int v = 0; v < 5; v++)
1635 {
1636 CPoint *pQuadPoint = &pCurrentQuad->m_aPoints[v];
1637
1638 if(v != 4)
1639 { // Don't use pivot to compute AABB
1640 if(pQuadPoint->x < Min.x)
1641 Min.x = pQuadPoint->x;
1642 if(pQuadPoint->y < Min.y)
1643 Min.y = pQuadPoint->y;
1644 if(pQuadPoint->x > Max.x)
1645 Max.x = pQuadPoint->x;
1646 if(pQuadPoint->y > Max.y)
1647 Max.y = pQuadPoint->y;
1648 }
1649
1650 // Don't check alignment with current point
1651 if(pQuadPoint == &pQuad->m_aPoints[PointIndex])
1652 continue;
1653
1654 // Don't check alignment with other selected points
1655 bool IsCurrentPointSelected = IsQuadSelected(Index: i) && (IsQuadCornerSelected(Index: v) || (v == PointIndex && PointIndex == 4));
1656 if(IsCurrentPointSelected)
1657 continue;
1658
1659 CheckAlignment(pQuadPoint);
1660 }
1661
1662 // Don't check alignment with center of selected quads
1663 if(!IsQuadSelected(Index: i))
1664 {
1665 CPoint Center = (Min + Max) / 2.0f;
1666 CheckAlignment(&Center);
1667 }
1668 }
1669
1670 // Finally concatenate both alignment vectors into the output
1671 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1672 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1673 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1674}
1675
1676void CEditor::ComputePointsAlignments(const std::shared_ptr<CLayerQuads> &pLayer, bool Pivot, int OffsetX, int OffsetY, std::vector<SAlignmentInfo> &vAlignments) const
1677{
1678 // This method is used to compute alignments from selected points
1679 // and only apply the closest alignment on X and Y to the offset.
1680
1681 vAlignments.clear();
1682 std::vector<SAlignmentInfo> vAllAlignments;
1683
1684 for(int Selected : m_vSelectedQuads)
1685 {
1686 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1687
1688 if(!Pivot)
1689 {
1690 for(int m = 0; m < 4; m++)
1691 {
1692 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
1693 {
1694 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: m, OffsetX, OffsetY, vAlignments&: vAllAlignments, Append: true);
1695 }
1696 }
1697 }
1698 else
1699 {
1700 ComputePointAlignments(pLayer, pQuad, QuadIndex: Selected, PointIndex: 4, OffsetX, OffsetY, vAlignments&: vAllAlignments, Append: true);
1701 }
1702 }
1703
1704 int SmallestDiffX, SmallestDiffY;
1705 SmallestDiffX = SmallestDiffY = std::numeric_limits<int>::max();
1706
1707 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1708
1709 for(const auto &Alignment : vAllAlignments)
1710 {
1711 int AbsDiff = absolute(a: Alignment.m_Diff);
1712 if(Alignment.m_Axis == EAxis::AXIS_X)
1713 {
1714 if(AbsDiff < SmallestDiffY)
1715 {
1716 SmallestDiffY = AbsDiff;
1717 vAlignmentsY.clear();
1718 }
1719 if(AbsDiff == SmallestDiffY)
1720 vAlignmentsY.emplace_back(args: Alignment);
1721 }
1722 else if(Alignment.m_Axis == EAxis::AXIS_Y)
1723 {
1724 if(AbsDiff < SmallestDiffX)
1725 {
1726 SmallestDiffX = AbsDiff;
1727 vAlignmentsX.clear();
1728 }
1729 if(AbsDiff == SmallestDiffX)
1730 vAlignmentsX.emplace_back(args: Alignment);
1731 }
1732 }
1733
1734 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1735 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1736 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1737}
1738
1739void CEditor::ComputeAABBAlignments(const std::shared_ptr<CLayerQuads> &pLayer, const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY, std::vector<SAlignmentInfo> &vAlignments) const
1740{
1741 vAlignments.clear();
1742 if(!g_Config.m_EdAlignQuads)
1743 return;
1744
1745 // This method is a bit different than the point alignment in the way where instead of trying to aling 1 point to all quads,
1746 // we try to align 5 points to all quads, these 5 points being 5 points of an AABB.
1747 // Otherwise, the concept is the same, we use the original position of the AABB to make the computations.
1748 int Threshold = f2fx(v: maximum(a: 5.0f, b: 10.0f * m_MouseWorldScale));
1749 int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1;
1750 std::vector<SAlignmentInfo> vAlignmentsX, vAlignmentsY;
1751
1752 bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed();
1753
1754 auto &&CheckAlignment = [&](CPoint &Aligned, int Point) {
1755 CPoint ToCheck = AABB.m_aPoints[Point] + ivec2(OffsetX, OffsetY);
1756 int DX = Aligned.x - ToCheck.x;
1757 int DY = Aligned.y - ToCheck.y;
1758 int DiffX = absolute(a: DX);
1759 int DiffY = absolute(a: DY);
1760
1761 if(DiffX <= Threshold && (!GridEnabled || DiffX == 0))
1762 {
1763 if(DiffX < SmallestDiffX)
1764 {
1765 SmallestDiffX = DiffX;
1766 vAlignmentsX.clear();
1767 }
1768
1769 if(DiffX == SmallestDiffX)
1770 {
1771 vAlignmentsX.push_back(x: SAlignmentInfo{
1772 .m_AlignedPoint: Aligned,
1773 {.m_X: AABB.m_aPoints[Point].y},
1774 .m_Axis: EAxis::AXIS_Y,
1775 .m_PointIndex: Point,
1776 .m_Diff: DX,
1777 });
1778 }
1779 }
1780
1781 if(DiffY <= Threshold && (!GridEnabled || DiffY == 0))
1782 {
1783 if(DiffY < SmallestDiffY)
1784 {
1785 SmallestDiffY = DiffY;
1786 vAlignmentsY.clear();
1787 }
1788
1789 if(DiffY == SmallestDiffY)
1790 {
1791 vAlignmentsY.push_back(x: SAlignmentInfo{
1792 .m_AlignedPoint: Aligned,
1793 {.m_X: AABB.m_aPoints[Point].x},
1794 .m_Axis: EAxis::AXIS_X,
1795 .m_PointIndex: Point,
1796 .m_Diff: DY,
1797 });
1798 }
1799 }
1800 };
1801
1802 auto &&CheckAABBAlignment = [&](CPoint &QuadMin, CPoint &QuadMax) {
1803 CPoint QuadCenter = (QuadMin + QuadMax) / 2.0f;
1804 CPoint aQuadPoints[5] = {
1805 QuadMin, // Top left
1806 {QuadMax.x, QuadMin.y}, // Top right
1807 {QuadMin.x, QuadMax.y}, // Bottom left
1808 QuadMax, // Bottom right
1809 QuadCenter,
1810 };
1811
1812 // Check all points with all the other points
1813 for(auto &QuadPoint : aQuadPoints)
1814 {
1815 // i is the quad point which is "aligned" and that we want to compare with
1816 for(int j = 0; j < 5; j++)
1817 {
1818 // j is the point we try to align
1819 CheckAlignment(QuadPoint, j);
1820 }
1821 }
1822 };
1823
1824 // Iterate through all quads of the current layer
1825 // Compute AABB of all quads and check if the dragged AABB can be aligned to this AABB.
1826 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
1827 {
1828 auto *pCurrentQuad = &pLayer->m_vQuads[i];
1829 if(IsQuadSelected(Index: i)) // Don't check with other selected quads
1830 continue;
1831
1832 // Get AABB of this quad
1833 CPoint QuadMin = pCurrentQuad->m_aPoints[0], QuadMax = pCurrentQuad->m_aPoints[0];
1834 for(int v = 1; v < 4; v++)
1835 {
1836 QuadMin.x = minimum(a: QuadMin.x, b: pCurrentQuad->m_aPoints[v].x);
1837 QuadMin.y = minimum(a: QuadMin.y, b: pCurrentQuad->m_aPoints[v].y);
1838 QuadMax.x = maximum(a: QuadMax.x, b: pCurrentQuad->m_aPoints[v].x);
1839 QuadMax.y = maximum(a: QuadMax.y, b: pCurrentQuad->m_aPoints[v].y);
1840 }
1841
1842 CheckAABBAlignment(QuadMin, QuadMax);
1843 }
1844
1845 // Finally, concatenate both alignment vectors into the output
1846 vAlignments.reserve(n: vAlignmentsX.size() + vAlignmentsY.size());
1847 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsX.begin(), last: vAlignmentsX.end());
1848 vAlignments.insert(position: vAlignments.end(), first: vAlignmentsY.begin(), last: vAlignmentsY.end());
1849}
1850
1851void CEditor::DrawPointAlignments(const std::vector<SAlignmentInfo> &vAlignments, int OffsetX, int OffsetY) const
1852{
1853 if(!g_Config.m_EdAlignQuads)
1854 return;
1855
1856 // Drawing an alignment is easy, we convert fixed to float for the aligned point coords
1857 // and we also convert the "changing" value after applying the offset (which might be edited to actually align the value with the alignment).
1858 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
1859 for(const SAlignmentInfo &Alignment : vAlignments)
1860 {
1861 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1862 if(Alignment.m_Axis == EAxis::AXIS_X)
1863 { // Alignment on X axis is same Y values but different X values
1864 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), fx2f(v: Alignment.m_X + OffsetX - Alignment.m_AlignedPoint.x), 1.0f * m_MouseWorldScale);
1865 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1866 }
1867 else if(Alignment.m_Axis == EAxis::AXIS_Y)
1868 { // Alignment on Y axis is same X values but different Y values
1869 IGraphics::CQuadItem Line(fx2f(v: Alignment.m_AlignedPoint.x), fx2f(v: Alignment.m_AlignedPoint.y), 1.0f * m_MouseWorldScale, fx2f(v: Alignment.m_Y + OffsetY - Alignment.m_AlignedPoint.y));
1870 Graphics()->QuadsDrawTL(pArray: &Line, Num: 1);
1871 }
1872 }
1873}
1874
1875void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY) const
1876{
1877 // Drawing an AABB is simply converting the points from fixed to float
1878 // Then making lines out of quads and drawing them
1879 vec2 TL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].y + OffsetY)};
1880 vec2 TR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].y + OffsetY)};
1881 vec2 BL = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].y + OffsetY)};
1882 vec2 BR = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].y + OffsetY)};
1883 vec2 Center = {fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].x + OffsetX), fx2f(v: AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].y + OffsetY)};
1884
1885 // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine.
1886 IGraphics::CQuadItem Lines[4] = {
1887 {TL.x, TL.y, TR.x - TL.x, 1.0f * m_MouseWorldScale},
1888 {TL.x, TL.y, 1.0f * m_MouseWorldScale, BL.y - TL.y},
1889 {TR.x, TR.y, 1.0f * m_MouseWorldScale, BR.y - TR.y},
1890 {BL.x, BL.y, BR.x - BL.x, 1.0f * m_MouseWorldScale},
1891 };
1892 Graphics()->SetColor(r: 1, g: 0, b: 1, a: 1);
1893 Graphics()->QuadsDrawTL(pArray: Lines, Num: 4);
1894
1895 IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale);
1896 Graphics()->QuadsDraw(pArray: &CenterQuad, Num: 1);
1897}
1898
1899void CEditor::QuadSelectionAABB(const std::shared_ptr<CLayerQuads> &pLayer, SAxisAlignedBoundingBox &OutAABB)
1900{
1901 // Compute an englobing AABB of the current selection of quads
1902 CPoint Min{
1903 std::numeric_limits<int>::max(),
1904 std::numeric_limits<int>::max(),
1905 };
1906 CPoint Max{
1907 std::numeric_limits<int>::min(),
1908 std::numeric_limits<int>::min(),
1909 };
1910 for(int Selected : m_vSelectedQuads)
1911 {
1912 CQuad *pQuad = &pLayer->m_vQuads[Selected];
1913 for(int i = 0; i < 4; i++)
1914 {
1915 auto *pPoint = &pQuad->m_aPoints[i];
1916 Min.x = minimum(a: Min.x, b: pPoint->x);
1917 Min.y = minimum(a: Min.y, b: pPoint->y);
1918 Max.x = maximum(a: Max.x, b: pPoint->x);
1919 Max.y = maximum(a: Max.y, b: pPoint->y);
1920 }
1921 }
1922 CPoint Center = (Min + Max) / 2.0f;
1923 CPoint aPoints[SAxisAlignedBoundingBox::NUM_POINTS] = {
1924 Min, // Top left
1925 {Max.x, Min.y}, // Top right
1926 {Min.x, Max.y}, // Bottom left
1927 Max, // Bottom right
1928 Center,
1929 };
1930 mem_copy(dest: OutAABB.m_aPoints, source: aPoints, size: sizeof(CPoint) * SAxisAlignedBoundingBox::NUM_POINTS);
1931}
1932
1933void CEditor::ApplyAlignments(const std::vector<SAlignmentInfo> &vAlignments, int &OffsetX, int &OffsetY)
1934{
1935 if(vAlignments.empty())
1936 return;
1937
1938 // Find X and Y aligment
1939 const int *pAlignedX = nullptr;
1940 const int *pAlignedY = nullptr;
1941
1942 // To Find the alignments we simply iterate through the vector of alignments and find the first
1943 // X and Y alignments.
1944 // Then, we use the saved m_Diff to adjust the offset
1945 int AdjustX = 0, AdjustY = 0;
1946 for(const SAlignmentInfo &Alignment : vAlignments)
1947 {
1948 if(Alignment.m_Axis == EAxis::AXIS_X && !pAlignedY)
1949 {
1950 pAlignedY = &Alignment.m_AlignedPoint.y;
1951 AdjustY = Alignment.m_Diff;
1952 }
1953 else if(Alignment.m_Axis == EAxis::AXIS_Y && !pAlignedX)
1954 {
1955 pAlignedX = &Alignment.m_AlignedPoint.x;
1956 AdjustX = Alignment.m_Diff;
1957 }
1958 }
1959
1960 // Adjust offset
1961 OffsetX += AdjustX;
1962 OffsetY += AdjustY;
1963}
1964
1965void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) const
1966{
1967 // This is used to preserve axis alignment when pressing `Shift`
1968 // Should be called before any other computation
1969 EAxis Axis = GetDragAxis(OffsetX, OffsetY);
1970 OffsetX = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_X) ? OffsetX : 0);
1971 OffsetY = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_Y) ? OffsetY : 0);
1972}
1973
1974void CEditor::DoQuad(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int Index)
1975{
1976 enum
1977 {
1978 OP_NONE = 0,
1979 OP_SELECT,
1980 OP_MOVE_ALL,
1981 OP_MOVE_PIVOT,
1982 OP_ROTATE,
1983 OP_CONTEXT_MENU,
1984 OP_DELETE,
1985 };
1986
1987 // some basic values
1988 void *pId = &pQuad->m_aPoints[4]; // use pivot addr as id
1989 static std::vector<std::vector<CPoint>> s_vvRotatePoints;
1990 static int s_Operation = OP_NONE;
1991 static float s_MouseXStart = 0.0f;
1992 static float s_MouseYStart = 0.0f;
1993 static float s_RotateAngle = 0;
1994 float wx = Ui()->MouseWorldX();
1995 float wy = Ui()->MouseWorldY();
1996 static CPoint s_OriginalPosition;
1997 static std::vector<SAlignmentInfo> s_PivotAlignments; // Alignments per pivot per quad
1998 static std::vector<SAlignmentInfo> s_vAABBAlignments; // Alignments for one AABB (single quad or selection of multiple quads)
1999 static SAxisAlignedBoundingBox s_SelectionAABB; // Selection AABB
2000 static ivec2 s_LastOffset; // Last offset, stored as static so we can use it to draw every frame
2001
2002 // get pivot
2003 float CenterX = fx2f(v: pQuad->m_aPoints[4].x);
2004 float CenterY = fx2f(v: pQuad->m_aPoints[4].y);
2005
2006 const bool IgnoreGrid = Input()->AltIsPressed();
2007
2008 auto &&GetDragOffset = [&]() -> ivec2 {
2009 float x = wx;
2010 float y = wy;
2011 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2012 MapView()->MapGrid()->SnapToGrid(x, y);
2013
2014 int OffsetX = f2fx(v: x) - s_OriginalPosition.x;
2015 int OffsetY = f2fx(v: y) - s_OriginalPosition.y;
2016
2017 return {OffsetX, OffsetY};
2018 };
2019
2020 // draw selection background
2021 if(IsQuadSelected(Index))
2022 {
2023 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
2024 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 7.0f * m_MouseWorldScale, 7.0f * m_MouseWorldScale);
2025 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2026 }
2027
2028 if(Ui()->CheckActiveItem(pId))
2029 {
2030 if(m_MouseDeltaWorld != vec2(0.0f, 0.0f))
2031 {
2032 if(s_Operation == OP_SELECT)
2033 {
2034 float x = s_MouseXStart - Ui()->MouseX();
2035 float y = s_MouseYStart - Ui()->MouseY();
2036
2037 if(x * x + y * y > 20.0f)
2038 {
2039 if(!IsQuadSelected(Index))
2040 SelectQuad(Index);
2041
2042 s_OriginalPosition = pQuad->m_aPoints[4];
2043
2044 if(Input()->ShiftIsPressed())
2045 {
2046 s_Operation = OP_MOVE_PIVOT;
2047 // When moving, we need to save the original position of all selected pivots
2048 for(int Selected : m_vSelectedQuads)
2049 {
2050 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2051 PreparePointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4);
2052 }
2053 }
2054 else
2055 {
2056 s_Operation = OP_MOVE_ALL;
2057 // When moving, we need to save the original position of all selected quads points
2058 for(int Selected : m_vSelectedQuads)
2059 {
2060 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2061 for(size_t v = 0; v < 5; v++)
2062 PreparePointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v);
2063 }
2064 // And precompute AABB of selection since it will not change during drag
2065 if(g_Config.m_EdAlignQuads)
2066 QuadSelectionAABB(pLayer, OutAABB&: s_SelectionAABB);
2067 }
2068 }
2069 }
2070
2071 // check if we only should move pivot
2072 if(s_Operation == OP_MOVE_PIVOT)
2073 {
2074 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2075
2076 s_LastOffset = GetDragOffset(); // Update offset
2077 ApplyAxisAlignment(OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y); // Apply axis alignment to the offset
2078
2079 ComputePointsAlignments(pLayer, Pivot: true, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y, vAlignments&: s_PivotAlignments);
2080 ApplyAlignments(vAlignments: s_PivotAlignments, OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2081
2082 for(auto &Selected : m_vSelectedQuads)
2083 {
2084 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2085 DoPointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: 4, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2086 }
2087 }
2088 else if(s_Operation == OP_MOVE_ALL)
2089 {
2090 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2091
2092 // Compute drag offset
2093 s_LastOffset = GetDragOffset();
2094 ApplyAxisAlignment(OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2095
2096 // Then compute possible alignments with the selection AABB
2097 ComputeAABBAlignments(pLayer, AABB: s_SelectionAABB, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y, vAlignments&: s_vAABBAlignments);
2098 // Apply alignments before drag
2099 ApplyAlignments(vAlignments: s_vAABBAlignments, OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2100 // Then do the drag
2101 for(int Selected : m_vSelectedQuads)
2102 {
2103 CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected];
2104 for(int v = 0; v < 5; v++)
2105 DoPointDrag(pLayer, pQuad: pCurrentQuad, QuadIndex: Selected, PointIndex: v, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2106 }
2107 }
2108 else if(s_Operation == OP_ROTATE)
2109 {
2110 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2111
2112 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
2113 {
2114 CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]];
2115 for(int v = 0; v < 4; v++)
2116 {
2117 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
2118 Rotate(pCenter: &pCurrentQuad->m_aPoints[4], pPoint: &pCurrentQuad->m_aPoints[v], Rotation: s_RotateAngle);
2119 }
2120 }
2121
2122 s_RotateAngle += Ui()->MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.0001f : 0.002f);
2123 }
2124 }
2125
2126 // Draw axis and aligments when moving
2127 if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
2128 {
2129 EAxis Axis = GetDragAxis(OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2130 DrawAxis(Axis, OriginalPoint&: s_OriginalPosition, Point&: pQuad->m_aPoints[4]);
2131
2132 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
2133 }
2134
2135 if(s_Operation == OP_MOVE_PIVOT)
2136 DrawPointAlignments(vAlignments: s_PivotAlignments, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2137
2138 if(s_Operation == OP_MOVE_ALL)
2139 {
2140 DrawPointAlignments(vAlignments: s_vAABBAlignments, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2141
2142 if(g_Config.m_EdShowQuadsRect)
2143 DrawAABB(AABB: s_SelectionAABB, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2144 }
2145
2146 if(s_Operation == OP_CONTEXT_MENU)
2147 {
2148 if(!Ui()->MouseButton(Index: 1))
2149 {
2150 if(m_vSelectedLayers.size() == 1)
2151 {
2152 m_SelectedQuadIndex = FindSelectedQuadIndex(Index);
2153
2154 static SPopupMenuId s_PopupQuadId;
2155 Ui()->DoPopupMenu(pId: &s_PopupQuadId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 198, pContext: this, pfnFunc: PopupQuad);
2156 Ui()->DisableMouseLock();
2157 }
2158 s_Operation = OP_NONE;
2159 Ui()->SetActiveItem(nullptr);
2160 }
2161 }
2162 else if(s_Operation == OP_DELETE)
2163 {
2164 if(!Ui()->MouseButton(Index: 1))
2165 {
2166 if(m_vSelectedLayers.size() == 1)
2167 {
2168 Ui()->DisableMouseLock();
2169 m_Map.OnModify();
2170 DeleteSelectedQuads();
2171 }
2172 s_Operation = OP_NONE;
2173 Ui()->SetActiveItem(nullptr);
2174 }
2175 }
2176 else if(s_Operation == OP_ROTATE)
2177 {
2178 if(Ui()->MouseButton(Index: 0))
2179 {
2180 Ui()->DisableMouseLock();
2181 s_Operation = OP_NONE;
2182 Ui()->SetActiveItem(nullptr);
2183 m_QuadTracker.EndQuadTrack();
2184 }
2185 else if(Ui()->MouseButton(Index: 1))
2186 {
2187 Ui()->DisableMouseLock();
2188 s_Operation = OP_NONE;
2189 Ui()->SetActiveItem(nullptr);
2190
2191 // Reset points to old position
2192 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
2193 {
2194 CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]];
2195 for(int v = 0; v < 4; v++)
2196 pCurrentQuad->m_aPoints[v] = s_vvRotatePoints[i][v];
2197 }
2198 }
2199 }
2200 else
2201 {
2202 if(!Ui()->MouseButton(Index: 0))
2203 {
2204 if(s_Operation == OP_SELECT)
2205 {
2206 if(Input()->ShiftIsPressed())
2207 ToggleSelectQuad(Index);
2208 else
2209 SelectQuad(Index);
2210 }
2211 else if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL)
2212 {
2213 m_QuadTracker.EndQuadTrack();
2214 }
2215
2216 Ui()->DisableMouseLock();
2217 s_Operation = OP_NONE;
2218 Ui()->SetActiveItem(nullptr);
2219
2220 s_LastOffset = ivec2();
2221 s_OriginalPosition = ivec2();
2222 s_vAABBAlignments.clear();
2223 s_PivotAlignments.clear();
2224 }
2225 }
2226
2227 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2228 }
2229 else if(Input()->KeyPress(Key: KEY_R) && !m_vSelectedQuads.empty() && m_Dialog == DIALOG_NONE)
2230 {
2231 Ui()->EnableMouseLock(pId);
2232 Ui()->SetActiveItem(pId);
2233 s_Operation = OP_ROTATE;
2234 s_RotateAngle = 0;
2235
2236 s_vvRotatePoints.clear();
2237 s_vvRotatePoints.resize(new_size: m_vSelectedQuads.size());
2238 for(size_t i = 0; i < m_vSelectedQuads.size(); ++i)
2239 {
2240 CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]];
2241
2242 s_vvRotatePoints[i].resize(new_size: 4);
2243 s_vvRotatePoints[i][0] = pCurrentQuad->m_aPoints[0];
2244 s_vvRotatePoints[i][1] = pCurrentQuad->m_aPoints[1];
2245 s_vvRotatePoints[i][2] = pCurrentQuad->m_aPoints[2];
2246 s_vvRotatePoints[i][3] = pCurrentQuad->m_aPoints[3];
2247 }
2248 }
2249 else if(Ui()->HotItem() == pId)
2250 {
2251 ms_pUiGotContext = pId;
2252
2253 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2254 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move pivot. Hold alt to ignore grid. Hold shift and right click to delete.");
2255
2256 if(Ui()->MouseButton(Index: 0))
2257 {
2258 Ui()->SetActiveItem(pId);
2259
2260 s_MouseXStart = Ui()->MouseX();
2261 s_MouseYStart = Ui()->MouseY();
2262
2263 s_Operation = OP_SELECT;
2264 }
2265 else if(Ui()->MouseButton(Index: 1))
2266 {
2267 if(Input()->ShiftIsPressed())
2268 {
2269 s_Operation = OP_DELETE;
2270
2271 if(!IsQuadSelected(Index))
2272 SelectQuad(Index);
2273
2274 Ui()->SetActiveItem(pId);
2275 }
2276 else
2277 {
2278 s_Operation = OP_CONTEXT_MENU;
2279
2280 if(!IsQuadSelected(Index))
2281 SelectQuad(Index);
2282
2283 Ui()->SetActiveItem(pId);
2284 }
2285 }
2286 }
2287 else
2288 Graphics()->SetColor(r: 0, g: 1, b: 0, a: 1);
2289
2290 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale);
2291 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2292}
2293
2294void CEditor::DoQuadPoint(int LayerIndex, const std::shared_ptr<CLayerQuads> &pLayer, CQuad *pQuad, int QuadIndex, int V)
2295{
2296 void *pId = &pQuad->m_aPoints[V];
2297
2298 float wx = Ui()->MouseWorldX();
2299 float wy = Ui()->MouseWorldY();
2300
2301 float px = fx2f(v: pQuad->m_aPoints[V].x);
2302 float py = fx2f(v: pQuad->m_aPoints[V].y);
2303
2304 // draw selection background
2305 if(IsQuadPointSelected(QuadIndex, Index: V))
2306 {
2307 Graphics()->SetColor(r: 0, g: 0, b: 0, a: 1);
2308 IGraphics::CQuadItem QuadItem(px, py, 7.0f * m_MouseWorldScale, 7.0f * m_MouseWorldScale);
2309 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2310 }
2311
2312 enum
2313 {
2314 OP_NONE = 0,
2315 OP_SELECT,
2316 OP_MOVEPOINT,
2317 OP_MOVEUV,
2318 OP_CONTEXT_MENU
2319 };
2320
2321 static int s_Operation = OP_NONE;
2322 static float s_MouseXStart = 0.0f;
2323 static float s_MouseYStart = 0.0f;
2324 static CPoint s_OriginalPoint;
2325 static std::vector<SAlignmentInfo> s_Alignments; // Alignments
2326 static ivec2 s_LastOffset;
2327
2328 const bool IgnoreGrid = Input()->AltIsPressed();
2329
2330 auto &&GetDragOffset = [&]() -> ivec2 {
2331 float x = wx;
2332 float y = wy;
2333 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2334 MapView()->MapGrid()->SnapToGrid(x, y);
2335
2336 int OffsetX = f2fx(v: x) - s_OriginalPoint.x;
2337 int OffsetY = f2fx(v: y) - s_OriginalPoint.y;
2338
2339 return {OffsetX, OffsetY};
2340 };
2341
2342 if(Ui()->CheckActiveItem(pId))
2343 {
2344 if(m_MouseDeltaWorld != vec2(0.0f, 0.0f))
2345 {
2346 if(s_Operation == OP_SELECT)
2347 {
2348 float x = s_MouseXStart - Ui()->MouseX();
2349 float y = s_MouseYStart - Ui()->MouseY();
2350
2351 if(x * x + y * y > 20.0f)
2352 {
2353 if(!IsQuadPointSelected(QuadIndex, Index: V))
2354 SelectQuadPoint(QuadIndex, Index: V);
2355
2356 if(Input()->ShiftIsPressed())
2357 {
2358 s_Operation = OP_MOVEUV;
2359 Ui()->EnableMouseLock(pId);
2360 }
2361 else
2362 {
2363 s_Operation = OP_MOVEPOINT;
2364 // Save original positions before moving
2365 s_OriginalPoint = pQuad->m_aPoints[V];
2366 for(int Selected : m_vSelectedQuads)
2367 {
2368 for(int m = 0; m < 4; m++)
2369 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
2370 PreparePointDrag(pLayer, pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m);
2371 }
2372 }
2373 }
2374 }
2375
2376 if(s_Operation == OP_MOVEPOINT)
2377 {
2378 m_QuadTracker.BeginQuadTrack(pLayer, vSelectedQuads: m_vSelectedQuads, GroupIndex: -1, LayerIndex);
2379
2380 s_LastOffset = GetDragOffset(); // Update offset
2381 ApplyAxisAlignment(OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y); // Apply axis alignment to offset
2382
2383 ComputePointsAlignments(pLayer, Pivot: false, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y, vAlignments&: s_Alignments);
2384 ApplyAlignments(vAlignments: s_Alignments, OffsetX&: s_LastOffset.x, OffsetY&: s_LastOffset.y);
2385
2386 for(int Selected : m_vSelectedQuads)
2387 {
2388 for(int m = 0; m < 4; m++)
2389 {
2390 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
2391 {
2392 DoPointDrag(pLayer, pQuad: &pLayer->m_vQuads[Selected], QuadIndex: Selected, PointIndex: m, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2393 }
2394 }
2395 }
2396 }
2397 else if(s_Operation == OP_MOVEUV)
2398 {
2399 int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3);
2400
2401 m_QuadTracker.BeginQuadPointPropTrack(pLayer, vSelectedQuads: m_vSelectedQuads, SelectedQuadPoints: SelectedPoints, GroupIndex: -1, LayerIndex);
2402 m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::PROP_TEX_U);
2403 m_QuadTracker.AddQuadPointPropTrack(Prop: EQuadPointProp::PROP_TEX_V);
2404
2405 for(int Selected : m_vSelectedQuads)
2406 {
2407 CQuad *pSelectedQuad = &pLayer->m_vQuads[Selected];
2408 for(int m = 0; m < 4; m++)
2409 {
2410 if(IsQuadPointSelected(QuadIndex: Selected, Index: m))
2411 {
2412 // 0,2;1,3 - line x
2413 // 0,1;2,3 - line y
2414
2415 pSelectedQuad->m_aTexcoords[m].x += f2fx(v: m_MouseDeltaWorld.x * 0.001f);
2416 pSelectedQuad->m_aTexcoords[(m + 2) % 4].x += f2fx(v: m_MouseDeltaWorld.x * 0.001f);
2417
2418 pSelectedQuad->m_aTexcoords[m].y += f2fx(v: m_MouseDeltaWorld.y * 0.001f);
2419 pSelectedQuad->m_aTexcoords[m ^ 1].y += f2fx(v: m_MouseDeltaWorld.y * 0.001f);
2420 }
2421 }
2422 }
2423 }
2424 }
2425
2426 // Draw axis and alignments when dragging
2427 if(s_Operation == OP_MOVEPOINT)
2428 {
2429 Graphics()->SetColor(r: 1, g: 0, b: 0.1f, a: 1);
2430
2431 // Axis
2432 EAxis Axis = GetDragAxis(OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2433 DrawAxis(Axis, OriginalPoint&: s_OriginalPoint, Point&: pQuad->m_aPoints[V]);
2434
2435 // Alignments
2436 DrawPointAlignments(vAlignments: s_Alignments, OffsetX: s_LastOffset.x, OffsetY: s_LastOffset.y);
2437
2438 str_copy(dst&: m_aTooltip, src: "Hold shift to keep alignment on one axis.");
2439 }
2440
2441 if(s_Operation == OP_CONTEXT_MENU)
2442 {
2443 if(!Ui()->MouseButton(Index: 1))
2444 {
2445 if(m_vSelectedLayers.size() == 1)
2446 {
2447 if(!IsQuadSelected(Index: QuadIndex))
2448 SelectQuad(Index: QuadIndex);
2449
2450 m_SelectedQuadPoint = V;
2451 m_SelectedQuadIndex = FindSelectedQuadIndex(Index: QuadIndex);
2452
2453 static SPopupMenuId s_PopupPointId;
2454 Ui()->DoPopupMenu(pId: &s_PopupPointId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 75, pContext: this, pfnFunc: PopupPoint);
2455 }
2456 Ui()->SetActiveItem(nullptr);
2457 }
2458 }
2459 else
2460 {
2461 if(!Ui()->MouseButton(Index: 0))
2462 {
2463 if(s_Operation == OP_SELECT)
2464 {
2465 if(Input()->ShiftIsPressed())
2466 ToggleSelectQuadPoint(QuadIndex, Index: V);
2467 else
2468 SelectQuadPoint(QuadIndex, Index: V);
2469 }
2470
2471 if(s_Operation == OP_MOVEPOINT)
2472 {
2473 m_QuadTracker.EndQuadTrack();
2474 }
2475 else if(s_Operation == OP_MOVEUV)
2476 {
2477 m_QuadTracker.EndQuadPointPropTrackAll();
2478 }
2479
2480 Ui()->DisableMouseLock();
2481 Ui()->SetActiveItem(nullptr);
2482 }
2483 }
2484
2485 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2486 }
2487 else if(Ui()->HotItem() == pId)
2488 {
2489 ms_pUiGotContext = pId;
2490
2491 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
2492 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold shift to move the texture. Hold alt to ignore grid.");
2493
2494 if(Ui()->MouseButton(Index: 0))
2495 {
2496 Ui()->SetActiveItem(pId);
2497
2498 s_MouseXStart = Ui()->MouseX();
2499 s_MouseYStart = Ui()->MouseY();
2500
2501 s_Operation = OP_SELECT;
2502 }
2503 else if(Ui()->MouseButton(Index: 1))
2504 {
2505 s_Operation = OP_CONTEXT_MENU;
2506
2507 Ui()->SetActiveItem(pId);
2508
2509 if(!IsQuadPointSelected(QuadIndex, Index: V))
2510 SelectQuadPoint(QuadIndex, Index: V);
2511 }
2512 }
2513 else
2514 Graphics()->SetColor(r: 1, g: 0, b: 0, a: 1);
2515
2516 IGraphics::CQuadItem QuadItem(px, py, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale);
2517 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2518}
2519
2520float CEditor::TriangleArea(vec2 A, vec2 B, vec2 C)
2521{
2522 return absolute(a: ((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) * 0.5f);
2523}
2524
2525bool CEditor::IsInTriangle(vec2 Point, vec2 A, vec2 B, vec2 C)
2526{
2527 // Normalize to increase precision
2528 vec2 Min(minimum(a: A.x, b: B.x, c: C.x), minimum(a: A.y, b: B.y, c: C.y));
2529 vec2 Max(maximum(a: A.x, b: B.x, c: C.x), maximum(a: A.y, b: B.y, c: C.y));
2530 vec2 Size(Max.x - Min.x, Max.y - Min.y);
2531
2532 if(Size.x < 0.0000001f || Size.y < 0.0000001f)
2533 return false;
2534
2535 vec2 Normal(1.f / Size.x, 1.f / Size.y);
2536
2537 A = (A - Min) * Normal;
2538 B = (B - Min) * Normal;
2539 C = (C - Min) * Normal;
2540 Point = (Point - Min) * Normal;
2541
2542 float Area = TriangleArea(A, B, C);
2543 return Area > 0.f && absolute(a: TriangleArea(A: Point, B: A, C: B) + TriangleArea(A: Point, B, C) + TriangleArea(A: Point, B: C, C: A) - Area) < 0.000001f;
2544}
2545
2546void CEditor::DoQuadKnife(int QuadIndex)
2547{
2548 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
2549 CQuad *pQuad = &pLayer->m_vQuads[QuadIndex];
2550
2551 const bool IgnoreGrid = Input()->AltIsPressed();
2552 float SnapRadius = 4.f * m_MouseWorldScale;
2553
2554 vec2 Mouse = vec2(Ui()->MouseWorldX(), Ui()->MouseWorldY());
2555 vec2 Point = Mouse;
2556
2557 vec2 v[4] = {
2558 vec2(fx2f(v: pQuad->m_aPoints[0].x), fx2f(v: pQuad->m_aPoints[0].y)),
2559 vec2(fx2f(v: pQuad->m_aPoints[1].x), fx2f(v: pQuad->m_aPoints[1].y)),
2560 vec2(fx2f(v: pQuad->m_aPoints[3].x), fx2f(v: pQuad->m_aPoints[3].y)),
2561 vec2(fx2f(v: pQuad->m_aPoints[2].x), fx2f(v: pQuad->m_aPoints[2].y))};
2562
2563 str_copy(dst&: m_aTooltip, src: "Left click inside the quad to select an area to slice. Hold alt to ignore grid. Right click to leave knife mode");
2564
2565 if(Ui()->MouseButtonClicked(Index: 1))
2566 {
2567 m_QuadKnifeActive = false;
2568 return;
2569 }
2570
2571 // Handle snapping
2572 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2573 {
2574 float CellSize = MapView()->MapGrid()->GridLineDistance();
2575 vec2 OnGrid(Mouse.x, Mouse.y);
2576 MapView()->MapGrid()->SnapToGrid(x&: OnGrid.x, y&: OnGrid.y);
2577
2578 if(IsInTriangle(Point: OnGrid, A: v[0], B: v[1], C: v[2]) || IsInTriangle(Point: OnGrid, A: v[0], B: v[3], C: v[2]))
2579 Point = OnGrid;
2580 else
2581 {
2582 float MinDistance = -1.f;
2583
2584 for(int i = 0; i < 4; i++)
2585 {
2586 int j = (i + 1) % 4;
2587 vec2 Min(minimum(a: v[i].x, b: v[j].x), minimum(a: v[i].y, b: v[j].y));
2588 vec2 Max(maximum(a: v[i].x, b: v[j].x), maximum(a: v[i].y, b: v[j].y));
2589
2590 if(in_range(a: OnGrid.y, lower: Min.y, upper: Max.y) && Max.y - Min.y > 0.0000001f)
2591 {
2592 vec2 OnEdge(v[i].x + (OnGrid.y - v[i].y) / (v[j].y - v[i].y) * (v[j].x - v[i].x), OnGrid.y);
2593 float Distance = absolute(a: OnGrid.x - OnEdge.x);
2594
2595 if(Distance < CellSize && (Distance < MinDistance || MinDistance < 0.f))
2596 {
2597 MinDistance = Distance;
2598 Point = OnEdge;
2599 }
2600 }
2601
2602 if(in_range(a: OnGrid.x, lower: Min.x, upper: Max.x) && Max.x - Min.x > 0.0000001f)
2603 {
2604 vec2 OnEdge(OnGrid.x, v[i].y + (OnGrid.x - v[i].x) / (v[j].x - v[i].x) * (v[j].y - v[i].y));
2605 float Distance = absolute(a: OnGrid.y - OnEdge.y);
2606
2607 if(Distance < CellSize && (Distance < MinDistance || MinDistance < 0.f))
2608 {
2609 MinDistance = Distance;
2610 Point = OnEdge;
2611 }
2612 }
2613 }
2614 }
2615 }
2616 else
2617 {
2618 float MinDistance = -1.f;
2619
2620 // Try snapping to corners
2621 for(const auto &x : v)
2622 {
2623 float Distance = distance(a: Mouse, b: x);
2624
2625 if(Distance <= SnapRadius && (Distance < MinDistance || MinDistance < 0.f))
2626 {
2627 MinDistance = Distance;
2628 Point = x;
2629 }
2630 }
2631
2632 if(MinDistance < 0.f)
2633 {
2634 // Try snapping to edges
2635 for(int i = 0; i < 4; i++)
2636 {
2637 int j = (i + 1) % 4;
2638 vec2 s(v[j] - v[i]);
2639
2640 float t = ((Mouse.x - v[i].x) * s.x + (Mouse.y - v[i].y) * s.y) / (s.x * s.x + s.y * s.y);
2641
2642 if(in_range(a: t, lower: 0.f, upper: 1.f))
2643 {
2644 vec2 OnEdge = vec2((v[i].x + t * s.x), (v[i].y + t * s.y));
2645 float Distance = distance(a: Mouse, b: OnEdge);
2646
2647 if(Distance <= SnapRadius && (Distance < MinDistance || MinDistance < 0.f))
2648 {
2649 MinDistance = Distance;
2650 Point = OnEdge;
2651 }
2652 }
2653 }
2654 }
2655 }
2656
2657 bool ValidPosition = IsInTriangle(Point, A: v[0], B: v[1], C: v[2]) || IsInTriangle(Point, A: v[0], B: v[3], C: v[2]);
2658
2659 if(Ui()->MouseButtonClicked(Index: 0) && ValidPosition)
2660 {
2661 m_aQuadKnifePoints[m_QuadKnifeCount] = Point;
2662 m_QuadKnifeCount++;
2663 }
2664
2665 if(m_QuadKnifeCount == 4)
2666 {
2667 if(IsInTriangle(Point: m_aQuadKnifePoints[3], A: m_aQuadKnifePoints[0], B: m_aQuadKnifePoints[1], C: m_aQuadKnifePoints[2]) ||
2668 IsInTriangle(Point: m_aQuadKnifePoints[1], A: m_aQuadKnifePoints[0], B: m_aQuadKnifePoints[2], C: m_aQuadKnifePoints[3]))
2669 {
2670 // Fix concave order
2671 std::swap(a&: m_aQuadKnifePoints[0], b&: m_aQuadKnifePoints[3]);
2672 std::swap(a&: m_aQuadKnifePoints[1], b&: m_aQuadKnifePoints[2]);
2673 }
2674
2675 std::swap(a&: m_aQuadKnifePoints[2], b&: m_aQuadKnifePoints[3]);
2676
2677 CQuad *pResult = pLayer->NewQuad(x: 64, y: 64, Width: 64, Height: 64);
2678 pQuad = &pLayer->m_vQuads[QuadIndex];
2679
2680 for(int i = 0; i < 4; i++)
2681 {
2682 int t = IsInTriangle(Point: m_aQuadKnifePoints[i], A: v[0], B: v[3], C: v[2]) ? 2 : 1;
2683
2684 vec2 A = vec2(fx2f(v: pQuad->m_aPoints[0].x), fx2f(v: pQuad->m_aPoints[0].y));
2685 vec2 B = vec2(fx2f(v: pQuad->m_aPoints[3].x), fx2f(v: pQuad->m_aPoints[3].y));
2686 vec2 C = vec2(fx2f(v: pQuad->m_aPoints[t].x), fx2f(v: pQuad->m_aPoints[t].y));
2687
2688 float TriArea = TriangleArea(A, B, C);
2689 float WeightA = TriangleArea(A: m_aQuadKnifePoints[i], B, C) / TriArea;
2690 float WeightB = TriangleArea(A: m_aQuadKnifePoints[i], B: C, C: A) / TriArea;
2691 float WeightC = TriangleArea(A: m_aQuadKnifePoints[i], B: A, C: B) / TriArea;
2692
2693 pResult->m_aColors[i].r = (int)std::round(x: pQuad->m_aColors[0].r * WeightA + pQuad->m_aColors[3].r * WeightB + pQuad->m_aColors[t].r * WeightC);
2694 pResult->m_aColors[i].g = (int)std::round(x: pQuad->m_aColors[0].g * WeightA + pQuad->m_aColors[3].g * WeightB + pQuad->m_aColors[t].g * WeightC);
2695 pResult->m_aColors[i].b = (int)std::round(x: pQuad->m_aColors[0].b * WeightA + pQuad->m_aColors[3].b * WeightB + pQuad->m_aColors[t].b * WeightC);
2696 pResult->m_aColors[i].a = (int)std::round(x: pQuad->m_aColors[0].a * WeightA + pQuad->m_aColors[3].a * WeightB + pQuad->m_aColors[t].a * WeightC);
2697
2698 pResult->m_aTexcoords[i].x = (int)std::round(x: pQuad->m_aTexcoords[0].x * WeightA + pQuad->m_aTexcoords[3].x * WeightB + pQuad->m_aTexcoords[t].x * WeightC);
2699 pResult->m_aTexcoords[i].y = (int)std::round(x: pQuad->m_aTexcoords[0].y * WeightA + pQuad->m_aTexcoords[3].y * WeightB + pQuad->m_aTexcoords[t].y * WeightC);
2700
2701 pResult->m_aPoints[i].x = f2fx(v: m_aQuadKnifePoints[i].x);
2702 pResult->m_aPoints[i].y = f2fx(v: m_aQuadKnifePoints[i].y);
2703 }
2704
2705 pResult->m_aPoints[4].x = ((pResult->m_aPoints[0].x + pResult->m_aPoints[3].x) / 2 + (pResult->m_aPoints[1].x + pResult->m_aPoints[2].x) / 2) / 2;
2706 pResult->m_aPoints[4].y = ((pResult->m_aPoints[0].y + pResult->m_aPoints[3].y) / 2 + (pResult->m_aPoints[1].y + pResult->m_aPoints[2].y) / 2) / 2;
2707
2708 m_QuadKnifeCount = 0;
2709 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionNewQuad>(args: this, args&: m_SelectedGroup, args&: m_vSelectedLayers[0]));
2710 }
2711
2712 // Render
2713 Graphics()->TextureClear();
2714 Graphics()->LinesBegin();
2715
2716 IGraphics::CLineItem aEdges[4] = {
2717 IGraphics::CLineItem(v[0].x, v[0].y, v[1].x, v[1].y),
2718 IGraphics::CLineItem(v[1].x, v[1].y, v[2].x, v[2].y),
2719 IGraphics::CLineItem(v[2].x, v[2].y, v[3].x, v[3].y),
2720 IGraphics::CLineItem(v[3].x, v[3].y, v[0].x, v[0].y)};
2721
2722 Graphics()->SetColor(r: 1.f, g: 0.5f, b: 0.f, a: 1.f);
2723 Graphics()->LinesDraw(pArray: aEdges, Num: 4);
2724
2725 IGraphics::CLineItem aLines[4];
2726 int LineCount = maximum(a: m_QuadKnifeCount - 1, b: 0);
2727
2728 for(int i = 0; i < LineCount; i++)
2729 aLines[i] = IGraphics::CLineItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, m_aQuadKnifePoints[i + 1].x, m_aQuadKnifePoints[i + 1].y);
2730
2731 Graphics()->SetColor(r: 1.f, g: 1.f, b: 1.f, a: 1.f);
2732 Graphics()->LinesDraw(pArray: aLines, Num: LineCount);
2733
2734 if(ValidPosition)
2735 {
2736 if(m_QuadKnifeCount > 0)
2737 {
2738 IGraphics::CLineItem LineCurrent(Point.x, Point.y, m_aQuadKnifePoints[m_QuadKnifeCount - 1].x, m_aQuadKnifePoints[m_QuadKnifeCount - 1].y);
2739 Graphics()->LinesDraw(pArray: &LineCurrent, Num: 1);
2740 }
2741
2742 if(m_QuadKnifeCount == 3)
2743 {
2744 IGraphics::CLineItem LineClose(Point.x, Point.y, m_aQuadKnifePoints[0].x, m_aQuadKnifePoints[0].y);
2745 Graphics()->LinesDraw(pArray: &LineClose, Num: 1);
2746 }
2747 }
2748
2749 Graphics()->LinesEnd();
2750 Graphics()->QuadsBegin();
2751
2752 IGraphics::CQuadItem aMarkers[4];
2753
2754 for(int i = 0; i < m_QuadKnifeCount; i++)
2755 aMarkers[i] = IGraphics::CQuadItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, 5.f * m_MouseWorldScale, 5.f * m_MouseWorldScale);
2756
2757 Graphics()->SetColor(r: 0.f, g: 0.f, b: 1.f, a: 1.f);
2758 Graphics()->QuadsDraw(pArray: aMarkers, Num: m_QuadKnifeCount);
2759
2760 if(ValidPosition)
2761 {
2762 IGraphics::CQuadItem MarkerCurrent(Point.x, Point.y, 5.f * m_MouseWorldScale, 5.f * m_MouseWorldScale);
2763 Graphics()->QuadsDraw(pArray: &MarkerCurrent, Num: 1);
2764 }
2765
2766 Graphics()->QuadsEnd();
2767}
2768
2769void CEditor::DoQuadEnvelopes(const std::vector<CQuad> &vQuads, IGraphics::CTextureHandle Texture)
2770{
2771 size_t Num = vQuads.size();
2772 std::shared_ptr<CEnvelope> *apEnvelope = new std::shared_ptr<CEnvelope>[Num];
2773 for(size_t i = 0; i < Num; i++)
2774 apEnvelope[i] = nullptr;
2775
2776 for(size_t i = 0; i < Num; i++)
2777 {
2778 if((m_ShowEnvelopePreview == SHOWENV_SELECTED && vQuads[i].m_PosEnv == m_SelectedEnvelope) || m_ShowEnvelopePreview == SHOWENV_ALL)
2779 if(vQuads[i].m_PosEnv >= 0 && vQuads[i].m_PosEnv < (int)m_Map.m_vpEnvelopes.size())
2780 apEnvelope[i] = m_Map.m_vpEnvelopes[vQuads[i].m_PosEnv];
2781 }
2782
2783 // Draw Lines
2784 Graphics()->TextureClear();
2785 Graphics()->LinesBegin();
2786 Graphics()->SetColor(r: 80.0f / 255, g: 150.0f / 255, b: 230.f / 255, a: 0.5f);
2787 for(size_t j = 0; j < Num; j++)
2788 {
2789 if(!apEnvelope[j])
2790 continue;
2791
2792 // QuadParams
2793 const CPoint *pPivotPoint = &vQuads[j].m_aPoints[4];
2794 for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size() - 1; i++)
2795 {
2796 ColorRGBA Result = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
2797 apEnvelope[j]->Eval(Time: apEnvelope[j]->m_vPoints[i].m_Time / 1000.0f + 0.000001f, Result, Channels: 2);
2798 vec2 Pos0 = vec2(fx2f(v: pPivotPoint->x) + Result.r, fx2f(v: pPivotPoint->y) + Result.g);
2799
2800 const int Steps = 15;
2801 for(int n = 1; n <= Steps; n++)
2802 {
2803 const float Time = mix(a: apEnvelope[j]->m_vPoints[i].m_Time, b: apEnvelope[j]->m_vPoints[i + 1].m_Time, amount: (float)n / Steps);
2804 Result = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
2805 apEnvelope[j]->Eval(Time: Time / 1000.0f - 0.000001f, Result, Channels: 2);
2806
2807 vec2 Pos1 = vec2(fx2f(v: pPivotPoint->x) + Result.r, fx2f(v: pPivotPoint->y) + Result.g);
2808
2809 IGraphics::CLineItem Line = IGraphics::CLineItem(Pos0.x, Pos0.y, Pos1.x, Pos1.y);
2810 Graphics()->LinesDraw(pArray: &Line, Num: 1);
2811
2812 Pos0 = Pos1;
2813 }
2814 }
2815 }
2816 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2817 Graphics()->LinesEnd();
2818
2819 // Draw Quads
2820 Graphics()->TextureSet(Texture);
2821 Graphics()->QuadsBegin();
2822
2823 for(size_t j = 0; j < Num; j++)
2824 {
2825 if(!apEnvelope[j])
2826 continue;
2827
2828 // QuadParams
2829 const CPoint *pPoints = vQuads[j].m_aPoints;
2830
2831 for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size(); i++)
2832 {
2833 // Calc Env Position
2834 float OffsetX = fx2f(v: apEnvelope[j]->m_vPoints[i].m_aValues[0]);
2835 float OffsetY = fx2f(v: apEnvelope[j]->m_vPoints[i].m_aValues[1]);
2836 float Rot = fx2f(v: apEnvelope[j]->m_vPoints[i].m_aValues[2]) / 360.0f * pi * 2;
2837
2838 // Set Colours
2839 float Alpha = (m_SelectedQuadEnvelope == vQuads[j].m_PosEnv && IsEnvPointSelected(Index: i)) ? 0.65f : 0.35f;
2840 IGraphics::CColorVertex aArray[4] = {
2841 IGraphics::CColorVertex(0, vQuads[j].m_aColors[0].r, vQuads[j].m_aColors[0].g, vQuads[j].m_aColors[0].b, Alpha),
2842 IGraphics::CColorVertex(1, vQuads[j].m_aColors[1].r, vQuads[j].m_aColors[1].g, vQuads[j].m_aColors[1].b, Alpha),
2843 IGraphics::CColorVertex(2, vQuads[j].m_aColors[2].r, vQuads[j].m_aColors[2].g, vQuads[j].m_aColors[2].b, Alpha),
2844 IGraphics::CColorVertex(3, vQuads[j].m_aColors[3].r, vQuads[j].m_aColors[3].g, vQuads[j].m_aColors[3].b, Alpha)};
2845 Graphics()->SetColorVertex(pArray: aArray, Num: 4);
2846
2847 // Rotation
2848 if(Rot != 0)
2849 {
2850 static CPoint aRotated[4];
2851 aRotated[0] = vQuads[j].m_aPoints[0];
2852 aRotated[1] = vQuads[j].m_aPoints[1];
2853 aRotated[2] = vQuads[j].m_aPoints[2];
2854 aRotated[3] = vQuads[j].m_aPoints[3];
2855 pPoints = aRotated;
2856
2857 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[0], Rotation: Rot);
2858 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[1], Rotation: Rot);
2859 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[2], Rotation: Rot);
2860 Rotate(pCenter: &vQuads[j].m_aPoints[4], pPoint: &aRotated[3], Rotation: Rot);
2861 }
2862
2863 // Set Texture Coords
2864 Graphics()->QuadsSetSubsetFree(
2865 x0: fx2f(v: vQuads[j].m_aTexcoords[0].x), y0: fx2f(v: vQuads[j].m_aTexcoords[0].y),
2866 x1: fx2f(v: vQuads[j].m_aTexcoords[1].x), y1: fx2f(v: vQuads[j].m_aTexcoords[1].y),
2867 x2: fx2f(v: vQuads[j].m_aTexcoords[2].x), y2: fx2f(v: vQuads[j].m_aTexcoords[2].y),
2868 x3: fx2f(v: vQuads[j].m_aTexcoords[3].x), y3: fx2f(v: vQuads[j].m_aTexcoords[3].y));
2869
2870 // Set Quad Coords & Draw
2871 IGraphics::CFreeformItem Freeform(
2872 fx2f(v: pPoints[0].x) + OffsetX, fx2f(v: pPoints[0].y) + OffsetY,
2873 fx2f(v: pPoints[1].x) + OffsetX, fx2f(v: pPoints[1].y) + OffsetY,
2874 fx2f(v: pPoints[2].x) + OffsetX, fx2f(v: pPoints[2].y) + OffsetY,
2875 fx2f(v: pPoints[3].x) + OffsetX, fx2f(v: pPoints[3].y) + OffsetY);
2876 Graphics()->QuadsDrawFreeform(pArray: &Freeform, Num: 1);
2877 }
2878 }
2879 Graphics()->QuadsEnd();
2880 Graphics()->TextureClear();
2881 Graphics()->QuadsBegin();
2882
2883 // Draw QuadPoints
2884 for(size_t j = 0; j < Num; j++)
2885 {
2886 if(!apEnvelope[j])
2887 continue;
2888
2889 for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size(); i++)
2890 DoQuadEnvPoint(pQuad: &vQuads[j], QIndex: j, pIndex: i);
2891 }
2892 Graphics()->QuadsEnd();
2893 delete[] apEnvelope;
2894}
2895
2896void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex)
2897{
2898 enum
2899 {
2900 OP_NONE = 0,
2901 OP_MOVE,
2902 OP_ROTATE,
2903 };
2904
2905 // some basic values
2906 static float s_LastWx = 0;
2907 static float s_LastWy = 0;
2908 static int s_Operation = OP_NONE;
2909 float wx = Ui()->MouseWorldX();
2910 float wy = Ui()->MouseWorldY();
2911 std::shared_ptr<CEnvelope> pEnvelope = m_Map.m_vpEnvelopes[pQuad->m_PosEnv];
2912 void *pId = &pEnvelope->m_vPoints[PIndex];
2913
2914 // get pivot
2915 float CenterX = fx2f(v: pQuad->m_aPoints[4].x) + fx2f(v: pEnvelope->m_vPoints[PIndex].m_aValues[0]);
2916 float CenterY = fx2f(v: pQuad->m_aPoints[4].y) + fx2f(v: pEnvelope->m_vPoints[PIndex].m_aValues[1]);
2917
2918 const bool IgnoreGrid = Input()->AltIsPressed();
2919
2920 if(Ui()->CheckActiveItem(pId) && m_CurrentQuadIndex == QIndex)
2921 {
2922 if(s_Operation == OP_MOVE)
2923 {
2924 if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid)
2925 {
2926 float x = wx;
2927 float y = wy;
2928 MapView()->MapGrid()->SnapToGrid(x, y);
2929 pEnvelope->m_vPoints[PIndex].m_aValues[0] = f2fx(v: x) - pQuad->m_aPoints[4].x;
2930 pEnvelope->m_vPoints[PIndex].m_aValues[1] = f2fx(v: y) - pQuad->m_aPoints[4].y;
2931 }
2932 else
2933 {
2934 pEnvelope->m_vPoints[PIndex].m_aValues[0] += f2fx(v: wx - s_LastWx);
2935 pEnvelope->m_vPoints[PIndex].m_aValues[1] += f2fx(v: wy - s_LastWy);
2936 }
2937 }
2938 else if(s_Operation == OP_ROTATE)
2939 pEnvelope->m_vPoints[PIndex].m_aValues[2] += 10 * Ui()->MouseDeltaX();
2940
2941 s_LastWx = wx;
2942 s_LastWy = wy;
2943
2944 if(!Ui()->MouseButton(Index: 0))
2945 {
2946 Ui()->DisableMouseLock();
2947 s_Operation = OP_NONE;
2948 Ui()->SetActiveItem(nullptr);
2949 }
2950
2951 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2952 }
2953 else if(Ui()->HotItem() == pId && m_CurrentQuadIndex == QIndex)
2954 {
2955 ms_pUiGotContext = pId;
2956
2957 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
2958 str_copy(dst&: m_aTooltip, src: "Left mouse button to move. Hold ctrl to rotate. Hold alt to ignore grid.");
2959
2960 if(Ui()->MouseButton(Index: 0))
2961 {
2962 if(Input()->ModifierIsPressed())
2963 {
2964 Ui()->EnableMouseLock(pId);
2965 s_Operation = OP_ROTATE;
2966
2967 SelectQuad(Index: QIndex);
2968 }
2969 else
2970 {
2971 s_Operation = OP_MOVE;
2972
2973 SelectQuad(Index: QIndex);
2974 }
2975
2976 SelectEnvPoint(Index: PIndex);
2977 m_SelectedQuadEnvelope = pQuad->m_PosEnv;
2978
2979 Ui()->SetActiveItem(pId);
2980
2981 s_LastWx = wx;
2982 s_LastWy = wy;
2983 }
2984 else
2985 {
2986 DeselectEnvPoints();
2987 m_SelectedQuadEnvelope = -1;
2988 }
2989 }
2990 else
2991 Graphics()->SetColor(r: 0.0f, g: 1.0f, b: 0.0f, a: 1.0f);
2992
2993 IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale);
2994 Graphics()->QuadsDraw(pArray: &QuadItem, Num: 1);
2995}
2996
2997void CEditor::DoMapEditor(CUIRect View)
2998{
2999 // render all good stuff
3000 if(!m_ShowPicker)
3001 {
3002 MapView()->RenderMap();
3003 }
3004 else
3005 {
3006 // fix aspect ratio of the image in the picker
3007 float Max = minimum(a: View.w, b: View.h);
3008 View.w = View.h = Max;
3009 }
3010
3011 const bool Inside = Ui()->MouseInside(pRect: &View);
3012
3013 // fetch mouse position
3014 float wx = Ui()->MouseWorldX();
3015 float wy = Ui()->MouseWorldY();
3016 float mx = Ui()->MouseX();
3017 float my = Ui()->MouseY();
3018
3019 static float s_StartWx = 0;
3020 static float s_StartWy = 0;
3021
3022 enum
3023 {
3024 OP_NONE = 0,
3025 OP_BRUSH_GRAB,
3026 OP_BRUSH_DRAW,
3027 OP_BRUSH_PAINT,
3028 OP_PAN_WORLD,
3029 OP_PAN_EDITOR,
3030 };
3031
3032 // remap the screen so it can display the whole tileset
3033 if(m_ShowPicker)
3034 {
3035 CUIRect Screen = *Ui()->Screen();
3036 float Size = 32.0f * 16.0f;
3037 float w = Size * (Screen.w / View.w);
3038 float h = Size * (Screen.h / View.h);
3039 float x = -(View.x / Screen.w) * w;
3040 float y = -(View.y / Screen.h) * h;
3041 wx = x + w * mx / Screen.w;
3042 wy = y + h * my / Screen.h;
3043 std::shared_ptr<CLayerTiles> pTileLayer = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
3044 if(pTileLayer)
3045 {
3046 Graphics()->MapScreen(TopLeftX: x, TopLeftY: y, BottomRightX: x + w, BottomRightY: y + h);
3047 m_pTilesetPicker->m_Image = pTileLayer->m_Image;
3048 if(m_BrushColorEnabled)
3049 {
3050 m_pTilesetPicker->m_Color = pTileLayer->m_Color;
3051 m_pTilesetPicker->m_Color.a = 255;
3052 }
3053 else
3054 {
3055 m_pTilesetPicker->m_Color = {255, 255, 255, 255};
3056 }
3057
3058 m_pTilesetPicker->m_Game = pTileLayer->m_Game;
3059 m_pTilesetPicker->m_Tele = pTileLayer->m_Tele;
3060 m_pTilesetPicker->m_Speedup = pTileLayer->m_Speedup;
3061 m_pTilesetPicker->m_Front = pTileLayer->m_Front;
3062 m_pTilesetPicker->m_Switch = pTileLayer->m_Switch;
3063 m_pTilesetPicker->m_Tune = pTileLayer->m_Tune;
3064
3065 m_pTilesetPicker->Render(Tileset: true);
3066
3067 if(m_ShowTileInfo != SHOW_TILE_OFF)
3068 m_pTilesetPicker->ShowInfo();
3069 }
3070 else
3071 {
3072 std::shared_ptr<CLayerQuads> pQuadLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
3073 if(pQuadLayer)
3074 {
3075 m_pQuadsetPicker->m_Image = pQuadLayer->m_Image;
3076 m_pQuadsetPicker->m_vQuads[0].m_aPoints[0].x = f2fx(v: View.x);
3077 m_pQuadsetPicker->m_vQuads[0].m_aPoints[0].y = f2fx(v: View.y);
3078 m_pQuadsetPicker->m_vQuads[0].m_aPoints[1].x = f2fx(v: (View.x + View.w));
3079 m_pQuadsetPicker->m_vQuads[0].m_aPoints[1].y = f2fx(v: View.y);
3080 m_pQuadsetPicker->m_vQuads[0].m_aPoints[2].x = f2fx(v: View.x);
3081 m_pQuadsetPicker->m_vQuads[0].m_aPoints[2].y = f2fx(v: (View.y + View.h));
3082 m_pQuadsetPicker->m_vQuads[0].m_aPoints[3].x = f2fx(v: (View.x + View.w));
3083 m_pQuadsetPicker->m_vQuads[0].m_aPoints[3].y = f2fx(v: (View.y + View.h));
3084 m_pQuadsetPicker->m_vQuads[0].m_aPoints[4].x = f2fx(v: (View.x + View.w / 2));
3085 m_pQuadsetPicker->m_vQuads[0].m_aPoints[4].y = f2fx(v: (View.y + View.h / 2));
3086 m_pQuadsetPicker->Render();
3087 }
3088 }
3089 }
3090
3091 static int s_Operation = OP_NONE;
3092
3093 // draw layer borders
3094 std::pair<int, std::shared_ptr<CLayer>> apEditLayers[128];
3095 size_t NumEditLayers = 0;
3096
3097 if(m_ShowPicker && GetSelectedLayer(Index: 0) && GetSelectedLayer(Index: 0)->m_Type == LAYERTYPE_TILES)
3098 {
3099 apEditLayers[0] = {0, m_pTilesetPicker};
3100 NumEditLayers++;
3101 }
3102 else if(m_ShowPicker)
3103 {
3104 apEditLayers[0] = {0, m_pQuadsetPicker};
3105 NumEditLayers++;
3106 }
3107 else
3108 {
3109 // pick a type of layers to edit, preferring Tiles layers.
3110 int EditingType = -1;
3111 for(size_t i = 0; i < m_vSelectedLayers.size(); i++)
3112 {
3113 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index: i);
3114 if(pLayer && (EditingType == -1 || pLayer->m_Type == LAYERTYPE_TILES))
3115 {
3116 EditingType = pLayer->m_Type;
3117 if(EditingType == LAYERTYPE_TILES)
3118 break;
3119 }
3120 }
3121 for(size_t i = 0; i < m_vSelectedLayers.size() && NumEditLayers < 128; i++)
3122 {
3123 apEditLayers[NumEditLayers] = {m_vSelectedLayers[i], GetSelectedLayerType(Index: i, Type: EditingType)};
3124 if(apEditLayers[NumEditLayers].second)
3125 {
3126 NumEditLayers++;
3127 }
3128 }
3129
3130 MapView()->RenderGroupBorder();
3131 MapView()->MapGrid()->OnRender(View);
3132 }
3133
3134 const bool ShouldPan = (Input()->ModifierIsPressed() && Ui()->MouseButton(Index: 0)) || Ui()->MouseButton(Index: 2);
3135 if(m_pContainerPanned == &m_MapEditorId)
3136 {
3137 // do panning
3138 if(ShouldPan)
3139 {
3140 if(Input()->ShiftIsPressed())
3141 s_Operation = OP_PAN_EDITOR;
3142 else
3143 s_Operation = OP_PAN_WORLD;
3144 }
3145 else
3146 s_Operation = OP_NONE;
3147
3148 if(s_Operation == OP_PAN_WORLD)
3149 MapView()->OffsetWorld(Offset: -Ui()->MouseDelta() * m_MouseWorldScale);
3150 else if(s_Operation == OP_PAN_EDITOR)
3151 MapView()->OffsetEditor(Offset: -Ui()->MouseDelta() * m_MouseWorldScale);
3152
3153 if(s_Operation == OP_NONE)
3154 m_pContainerPanned = nullptr;
3155 }
3156
3157 if(Inside)
3158 {
3159 Ui()->SetHotItem(&m_MapEditorId);
3160
3161 // do global operations like pan and zoom
3162 if(Ui()->CheckActiveItem(pId: nullptr) && (Ui()->MouseButton(Index: 0) || Ui()->MouseButton(Index: 2)))
3163 {
3164 s_StartWx = wx;
3165 s_StartWy = wy;
3166
3167 if(ShouldPan && m_pContainerPanned == nullptr)
3168 m_pContainerPanned = &m_MapEditorId;
3169 }
3170
3171 // brush editing
3172 if(Ui()->HotItem() == &m_MapEditorId)
3173 {
3174 if(m_ShowPicker)
3175 {
3176 std::shared_ptr<CLayer> pLayer = GetSelectedLayer(Index: 0);
3177 int Layer;
3178 if(pLayer == m_Map.m_pGameLayer)
3179 Layer = LAYER_GAME;
3180 else if(pLayer == m_Map.m_pFrontLayer)
3181 Layer = LAYER_FRONT;
3182 else if(pLayer == m_Map.m_pSwitchLayer)
3183 Layer = LAYER_SWITCH;
3184 else if(pLayer == m_Map.m_pTeleLayer)
3185 Layer = LAYER_TELE;
3186 else if(pLayer == m_Map.m_pSpeedupLayer)
3187 Layer = LAYER_SPEEDUP;
3188 else if(pLayer == m_Map.m_pTuneLayer)
3189 Layer = LAYER_TUNE;
3190 else
3191 Layer = NUM_LAYERS;
3192
3193 EExplanation Explanation;
3194 if(m_SelectEntitiesImage == "DDNet")
3195 Explanation = EExplanation::DDNET;
3196 else if(m_SelectEntitiesImage == "FNG")
3197 Explanation = EExplanation::FNG;
3198 else if(m_SelectEntitiesImage == "Race")
3199 Explanation = EExplanation::RACE;
3200 else if(m_SelectEntitiesImage == "Vanilla")
3201 Explanation = EExplanation::VANILLA;
3202 else if(m_SelectEntitiesImage == "blockworlds")
3203 Explanation = EExplanation::BLOCKWORLDS;
3204 else
3205 Explanation = EExplanation::NONE;
3206
3207 if(Layer != NUM_LAYERS)
3208 {
3209 const char *pExplanation = Explain(Explanation, Tile: (int)wx / 32 + (int)wy / 32 * 16, Layer);
3210 if(pExplanation)
3211 str_copy(dst&: m_aTooltip, src: pExplanation);
3212 }
3213 }
3214 else if(m_pBrush->IsEmpty() && GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS) != nullptr)
3215 str_copy(dst&: m_aTooltip, src: "Use left mouse button to drag and create a brush. Hold shift to select multiple quads. Press R to rotate selected quads. Use ctrl+right mouse to select layer.");
3216 else if(m_pBrush->IsEmpty())
3217 str_copy(dst&: m_aTooltip, src: "Use left mouse button to drag and create a brush. Use ctrl+right mouse to select layer.");
3218 else
3219 str_copy(dst&: m_aTooltip, src: "Use left mouse button to paint with the brush. Right button clears the brush.");
3220
3221 if(Ui()->CheckActiveItem(pId: &m_MapEditorId))
3222 {
3223 CUIRect r;
3224 r.x = s_StartWx;
3225 r.y = s_StartWy;
3226 r.w = wx - s_StartWx;
3227 r.h = wy - s_StartWy;
3228 if(r.w < 0)
3229 {
3230 r.x += r.w;
3231 r.w = -r.w;
3232 }
3233
3234 if(r.h < 0)
3235 {
3236 r.y += r.h;
3237 r.h = -r.h;
3238 }
3239
3240 if(s_Operation == OP_BRUSH_DRAW)
3241 {
3242 if(!m_pBrush->IsEmpty())
3243 {
3244 // draw with brush
3245 for(size_t k = 0; k < NumEditLayers; k++)
3246 {
3247 size_t BrushIndex = k % m_pBrush->m_vpLayers.size();
3248 if(apEditLayers[k].second->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type)
3249 {
3250 if(apEditLayers[k].second->m_Type == LAYERTYPE_TILES)
3251 {
3252 std::shared_ptr<CLayerTiles> pLayer = std::static_pointer_cast<CLayerTiles>(r: apEditLayers[k].second);
3253 std::shared_ptr<CLayerTiles> pBrushLayer = std::static_pointer_cast<CLayerTiles>(r: m_pBrush->m_vpLayers[BrushIndex]);
3254
3255 if(pLayer->m_Tele <= pBrushLayer->m_Tele && pLayer->m_Speedup <= pBrushLayer->m_Speedup && pLayer->m_Front <= pBrushLayer->m_Front && pLayer->m_Game <= pBrushLayer->m_Game && pLayer->m_Switch <= pBrushLayer->m_Switch && pLayer->m_Tune <= pBrushLayer->m_Tune)
3256 pLayer->BrushDraw(pBrush: pBrushLayer, wx, wy);
3257 }
3258 else
3259 {
3260 apEditLayers[k].second->BrushDraw(pBrush: m_pBrush->m_vpLayers[BrushIndex], x: wx, y: wy);
3261 }
3262 }
3263 }
3264 }
3265 }
3266 else if(s_Operation == OP_BRUSH_GRAB)
3267 {
3268 if(!Ui()->MouseButton(Index: 0))
3269 {
3270 std::shared_ptr<CLayerQuads> pQuadLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_QUADS));
3271 if(Input()->ShiftIsPressed() && pQuadLayer)
3272 {
3273 DeselectQuads();
3274 for(size_t i = 0; i < pQuadLayer->m_vQuads.size(); i++)
3275 {
3276 const CQuad &Quad = pQuadLayer->m_vQuads[i];
3277 vec2 Position = vec2(fx2f(v: Quad.m_aPoints[4].x), fx2f(v: Quad.m_aPoints[4].y));
3278 if(r.Inside(Point: Position) && !IsQuadSelected(Index: i))
3279 ToggleSelectQuad(Index: i);
3280 }
3281 }
3282 else
3283 {
3284 // grab brush
3285 char aBuf[256];
3286 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "grabbing %f %f %f %f", r.x, r.y, r.w, r.h);
3287 Console()->Print(Level: IConsole::OUTPUT_LEVEL_DEBUG, pFrom: "editor", pStr: aBuf);
3288
3289 // TODO: do all layers
3290 int Grabs = 0;
3291 for(size_t k = 0; k < NumEditLayers; k++)
3292 Grabs += apEditLayers[k].second->BrushGrab(pBrush: m_pBrush, Rect: r);
3293 if(Grabs == 0)
3294 m_pBrush->Clear();
3295
3296 DeselectQuads();
3297 DeselectQuadPoints();
3298 }
3299 }
3300 else
3301 {
3302 for(size_t k = 0; k < NumEditLayers; k++)
3303 apEditLayers[k].second->BrushSelecting(Rect: r);
3304 Ui()->MapScreen();
3305 }
3306 }
3307 else if(s_Operation == OP_BRUSH_PAINT)
3308 {
3309 if(!Ui()->MouseButton(Index: 0))
3310 {
3311 for(size_t k = 0; k < NumEditLayers; k++)
3312 {
3313 size_t BrushIndex = k;
3314 if(m_pBrush->m_vpLayers.size() != NumEditLayers)
3315 BrushIndex = 0;
3316 std::shared_ptr<CLayer> pBrush = m_pBrush->IsEmpty() ? nullptr : m_pBrush->m_vpLayers[BrushIndex];
3317 apEditLayers[k].second->FillSelection(Empty: m_pBrush->IsEmpty(), pBrush, Rect: r);
3318 }
3319 std::shared_ptr<IEditorAction> Action = std::make_shared<CEditorBrushDrawAction>(args: this, args&: m_SelectedGroup);
3320 m_EditorHistory.RecordAction(pAction: Action);
3321 }
3322 else
3323 {
3324 for(size_t k = 0; k < NumEditLayers; k++)
3325 apEditLayers[k].second->BrushSelecting(Rect: r);
3326 Ui()->MapScreen();
3327 }
3328 }
3329 }
3330 else
3331 {
3332 if(Ui()->MouseButton(Index: 1))
3333 {
3334 m_pBrush->Clear();
3335 }
3336
3337 if(!Input()->ModifierIsPressed() && Ui()->MouseButton(Index: 0) && s_Operation == OP_NONE && !m_QuadKnifeActive)
3338 {
3339 Ui()->SetActiveItem(&m_MapEditorId);
3340
3341 if(m_pBrush->IsEmpty())
3342 s_Operation = OP_BRUSH_GRAB;
3343 else
3344 {
3345 s_Operation = OP_BRUSH_DRAW;
3346 for(size_t k = 0; k < NumEditLayers; k++)
3347 {
3348 size_t BrushIndex = k;
3349 if(m_pBrush->m_vpLayers.size() != NumEditLayers)
3350 BrushIndex = 0;
3351
3352 if(apEditLayers[k].second->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type)
3353 apEditLayers[k].second->BrushPlace(pBrush: m_pBrush->m_vpLayers[BrushIndex], x: wx, y: wy);
3354 }
3355 }
3356
3357 std::shared_ptr<CLayerTiles> pLayer = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
3358 if(Input()->ShiftIsPressed() && pLayer)
3359 s_Operation = OP_BRUSH_PAINT;
3360 }
3361
3362 if(!m_pBrush->IsEmpty())
3363 {
3364 m_pBrush->m_OffsetX = -(int)wx;
3365 m_pBrush->m_OffsetY = -(int)wy;
3366 for(const auto &pLayer : m_pBrush->m_vpLayers)
3367 {
3368 if(pLayer->m_Type == LAYERTYPE_TILES)
3369 {
3370 m_pBrush->m_OffsetX = -(int)(wx / 32.0f) * 32;
3371 m_pBrush->m_OffsetY = -(int)(wy / 32.0f) * 32;
3372 break;
3373 }
3374 }
3375
3376 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
3377 if(!m_ShowPicker && pGroup)
3378 {
3379 m_pBrush->m_OffsetX += pGroup->m_OffsetX;
3380 m_pBrush->m_OffsetY += pGroup->m_OffsetY;
3381 m_pBrush->m_ParallaxX = pGroup->m_ParallaxX;
3382 m_pBrush->m_ParallaxY = pGroup->m_ParallaxY;
3383 m_pBrush->Render();
3384 float w, h;
3385 m_pBrush->GetSize(pWidth: &w, pHeight: &h);
3386
3387 IGraphics::CLineItem Array[4] = {
3388 IGraphics::CLineItem(0, 0, w, 0),
3389 IGraphics::CLineItem(w, 0, w, h),
3390 IGraphics::CLineItem(w, h, 0, h),
3391 IGraphics::CLineItem(0, h, 0, 0)};
3392 Graphics()->TextureClear();
3393 Graphics()->LinesBegin();
3394 Graphics()->LinesDraw(pArray: Array, Num: 4);
3395 Graphics()->LinesEnd();
3396 }
3397 }
3398 }
3399 }
3400
3401 // quad & sound editing
3402 {
3403 if(!m_ShowPicker && m_pBrush->IsEmpty())
3404 {
3405 // fetch layers
3406 std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
3407 if(pGroup)
3408 pGroup->MapScreen();
3409
3410 for(size_t k = 0; k < NumEditLayers; k++)
3411 {
3412 auto &[LayerIndex, pEditLayer] = apEditLayers[k];
3413
3414 if(pEditLayer->m_Type == LAYERTYPE_QUADS)
3415 {
3416 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: pEditLayer);
3417
3418 if(m_ShowEnvelopePreview == SHOWENV_NONE)
3419 m_ShowEnvelopePreview = SHOWENV_ALL;
3420
3421 if(m_QuadKnifeActive)
3422 DoQuadKnife(QuadIndex: m_vSelectedQuads[m_SelectedQuadIndex]);
3423 else
3424 {
3425 SetHotQuadPoint(pLayer);
3426
3427 Graphics()->TextureClear();
3428 Graphics()->QuadsBegin();
3429 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
3430 {
3431 for(int v = 0; v < 4; v++)
3432 DoQuadPoint(LayerIndex, pLayer, pQuad: &pLayer->m_vQuads[i], QuadIndex: i, V: v);
3433
3434 DoQuad(LayerIndex, pLayer, pQuad: &pLayer->m_vQuads[i], Index: i);
3435 }
3436 Graphics()->QuadsEnd();
3437 }
3438 }
3439
3440 if(pEditLayer->m_Type == LAYERTYPE_SOUNDS)
3441 {
3442 std::shared_ptr<CLayerSounds> pLayer = std::static_pointer_cast<CLayerSounds>(r: pEditLayer);
3443
3444 Graphics()->TextureClear();
3445 Graphics()->QuadsBegin();
3446 for(size_t i = 0; i < pLayer->m_vSources.size(); i++)
3447 {
3448 DoSoundSource(LayerIndex, pSource: &pLayer->m_vSources[i], Index: i);
3449 }
3450 Graphics()->QuadsEnd();
3451 }
3452 }
3453
3454 Ui()->MapScreen();
3455 }
3456 }
3457
3458 // menu proof selection
3459 if(MapView()->ProofMode()->IsModeMenu() && !m_ShowPicker)
3460 {
3461 MapView()->ProofMode()->ResetMenuBackgroundPositions();
3462 for(int i = 0; i < (int)MapView()->ProofMode()->m_vMenuBackgroundPositions.size(); i++)
3463 {
3464 vec2 Pos = MapView()->ProofMode()->m_vMenuBackgroundPositions[i];
3465 Pos += MapView()->GetWorldOffset() - MapView()->ProofMode()->m_vMenuBackgroundPositions[MapView()->ProofMode()->m_CurrentMenuProofIndex];
3466 Pos.y -= 3.0f;
3467
3468 if(distance(a: Pos, b: m_MouseWorldNoParaPos) <= 20.0f)
3469 {
3470 Ui()->SetHotItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]);
3471
3472 if(i != MapView()->ProofMode()->m_CurrentMenuProofIndex && Ui()->CheckActiveItem(pId: &MapView()->ProofMode()->m_vMenuBackgroundPositions[i]))
3473 {
3474 if(!Ui()->MouseButton(Index: 0))
3475 {
3476 MapView()->ProofMode()->m_CurrentMenuProofIndex = i;
3477 MapView()->SetWorldOffset(MapView()->ProofMode()->m_vMenuBackgroundPositions[i]);
3478 Ui()->SetActiveItem(nullptr);
3479 }
3480 }
3481 else if(Ui()->HotItem() == &MapView()->ProofMode()->m_vMenuBackgroundPositions[i])
3482 {
3483 char aTooltipPrefix[32] = "Switch proof position to";
3484 if(i == MapView()->ProofMode()->m_CurrentMenuProofIndex)
3485 str_copy(dst&: aTooltipPrefix, src: "Current proof position at");
3486
3487 char aNumBuf[8];
3488 if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST))
3489 str_format(buffer: aNumBuf, buffer_size: sizeof(aNumBuf), format: "#%d", i + 1);
3490 else
3491 aNumBuf[0] = '\0';
3492
3493 char aTooltipPositions[128];
3494 str_format(buffer: aTooltipPositions, buffer_size: sizeof(aTooltipPositions), format: "%s %s", MapView()->ProofMode()->m_vpMenuBackgroundPositionNames[i], aNumBuf);
3495
3496 for(int k : MapView()->ProofMode()->m_vMenuBackgroundCollisions.at(n: i))
3497 {
3498 if(k == MapView()->ProofMode()->m_CurrentMenuProofIndex)
3499 str_copy(dst&: aTooltipPrefix, src: "Current proof position at");
3500
3501 Pos = MapView()->ProofMode()->m_vMenuBackgroundPositions[k];
3502 Pos += MapView()->GetWorldOffset() - MapView()->ProofMode()->m_vMenuBackgroundPositions[MapView()->ProofMode()->m_CurrentMenuProofIndex];
3503 Pos.y -= 3.0f;
3504
3505 if(distance(a: Pos, b: m_MouseWorldNoParaPos) > 20.0f)
3506 continue;
3507
3508 if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST))
3509 str_format(buffer: aNumBuf, buffer_size: sizeof(aNumBuf), format: "#%d", k + 1);
3510 else
3511 aNumBuf[0] = '\0';
3512
3513 char aTooltipPositionsCopy[128];
3514 str_copy(dst&: aTooltipPositionsCopy, src: aTooltipPositions);
3515 str_format(buffer: aTooltipPositions, buffer_size: sizeof(aTooltipPositions), format: "%s, %s %s", aTooltipPositionsCopy, MapView()->ProofMode()->m_vpMenuBackgroundPositionNames[k], aNumBuf);
3516 }
3517 str_format(buffer: m_aMenuBackgroundTooltip, buffer_size: sizeof(m_aMenuBackgroundTooltip), format: "%s %s", aTooltipPrefix, aTooltipPositions);
3518
3519 str_copy(dst&: m_aTooltip, src: m_aMenuBackgroundTooltip);
3520 if(Ui()->MouseButton(Index: 0))
3521 Ui()->SetActiveItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]);
3522 }
3523 break;
3524 }
3525 }
3526 }
3527
3528 if(Ui()->CheckActiveItem(pId: &m_MapEditorId))
3529 {
3530 // release mouse
3531 if(!Ui()->MouseButton(Index: 0))
3532 {
3533 if(s_Operation == OP_BRUSH_DRAW)
3534 {
3535 std::shared_ptr<IEditorAction> Action = std::make_shared<CEditorBrushDrawAction>(args: this, args&: m_SelectedGroup);
3536
3537 if(!Action->IsEmpty()) // Avoid recording tile draw action when placing quads only
3538 m_EditorHistory.RecordAction(pAction: Action);
3539 }
3540
3541 s_Operation = OP_NONE;
3542 Ui()->SetActiveItem(nullptr);
3543 }
3544 }
3545 if(!Input()->ModifierIsPressed() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
3546 {
3547 float PanSpeed = Input()->ShiftIsPressed() ? 200.0f : 64.0f;
3548 if(Input()->KeyPress(Key: KEY_A))
3549 MapView()->OffsetWorld(Offset: {-PanSpeed * m_MouseWorldScale, 0});
3550 else if(Input()->KeyPress(Key: KEY_D))
3551 MapView()->OffsetWorld(Offset: {PanSpeed * m_MouseWorldScale, 0});
3552 if(Input()->KeyPress(Key: KEY_W))
3553 MapView()->OffsetWorld(Offset: {0, -PanSpeed * m_MouseWorldScale});
3554 else if(Input()->KeyPress(Key: KEY_S))
3555 MapView()->OffsetWorld(Offset: {0, PanSpeed * m_MouseWorldScale});
3556 }
3557 }
3558 else if(Ui()->CheckActiveItem(pId: &m_MapEditorId))
3559 {
3560 // release mouse
3561 if(!Ui()->MouseButton(Index: 0))
3562 {
3563 s_Operation = OP_NONE;
3564 Ui()->SetActiveItem(nullptr);
3565 }
3566 }
3567
3568 if(!m_ShowPicker && GetSelectedGroup() && GetSelectedGroup()->m_UseClipping)
3569 {
3570 std::shared_ptr<CLayerGroup> pGameGroup = m_Map.m_pGameGroup;
3571 pGameGroup->MapScreen();
3572
3573 Graphics()->TextureClear();
3574 Graphics()->LinesBegin();
3575
3576 CUIRect r;
3577 r.x = GetSelectedGroup()->m_ClipX;
3578 r.y = GetSelectedGroup()->m_ClipY;
3579 r.w = GetSelectedGroup()->m_ClipW;
3580 r.h = GetSelectedGroup()->m_ClipH;
3581
3582 IGraphics::CLineItem Array[4] = {
3583 IGraphics::CLineItem(r.x, r.y, r.x + r.w, r.y),
3584 IGraphics::CLineItem(r.x + r.w, r.y, r.x + r.w, r.y + r.h),
3585 IGraphics::CLineItem(r.x + r.w, r.y + r.h, r.x, r.y + r.h),
3586 IGraphics::CLineItem(r.x, r.y + r.h, r.x, r.y)};
3587 Graphics()->SetColor(r: 1, g: 0, b: 0, a: 1);
3588 Graphics()->LinesDraw(pArray: Array, Num: 4);
3589
3590 Graphics()->LinesEnd();
3591 }
3592
3593 MapView()->ProofMode()->RenderScreenSizes();
3594
3595 if(!m_ShowPicker && m_ShowTileInfo != SHOW_TILE_OFF && m_ShowEnvelopePreview != SHOWENV_NONE && GetSelectedLayer(Index: 0) && GetSelectedLayer(Index: 0)->m_Type == LAYERTYPE_QUADS)
3596 {
3597 GetSelectedGroup()->MapScreen();
3598
3599 std::shared_ptr<CLayerQuads> pLayer = std::static_pointer_cast<CLayerQuads>(r: GetSelectedLayer(Index: 0));
3600 IGraphics::CTextureHandle Texture;
3601 if(pLayer->m_Image >= 0 && pLayer->m_Image < (int)m_Map.m_vpImages.size())
3602 Texture = m_Map.m_vpImages[pLayer->m_Image]->m_Texture;
3603
3604 DoQuadEnvelopes(vQuads: pLayer->m_vQuads, Texture);
3605 m_ShowEnvelopePreview = SHOWENV_NONE;
3606 }
3607
3608 Ui()->MapScreen();
3609}
3610
3611void CEditor::SetHotQuadPoint(const std::shared_ptr<CLayerQuads> &pLayer)
3612{
3613 float wx = Ui()->MouseWorldX();
3614 float wy = Ui()->MouseWorldY();
3615
3616 float MinDist = 500.0f;
3617 void *pMinPoint = nullptr;
3618
3619 auto UpdateMinimum = [&](float px, float py, void *pId) {
3620 float dx = (px - wx) / m_MouseWorldScale;
3621 float dy = (py - wy) / m_MouseWorldScale;
3622
3623 float CurrDist = dx * dx + dy * dy;
3624 if(CurrDist < MinDist)
3625 {
3626 MinDist = CurrDist;
3627 pMinPoint = pId;
3628 return true;
3629 }
3630 return false;
3631 };
3632
3633 for(size_t i = 0; i < pLayer->m_vQuads.size(); i++)
3634 {
3635 CQuad &Quad = pLayer->m_vQuads.at(n: i);
3636
3637 if(m_ShowTileInfo != SHOW_TILE_OFF && m_ShowEnvelopePreview != SHOWENV_NONE && Quad.m_PosEnv >= 0)
3638 {
3639 for(auto &EnvPoint : m_Map.m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints)
3640 {
3641 float px = fx2f(v: Quad.m_aPoints[4].x) + fx2f(v: EnvPoint.m_aValues[0]);
3642 float py = fx2f(v: Quad.m_aPoints[4].y) + fx2f(v: EnvPoint.m_aValues[1]);
3643 if(UpdateMinimum(px, py, &EnvPoint))
3644 m_CurrentQuadIndex = i;
3645 }
3646 }
3647
3648 for(auto &Point : Quad.m_aPoints)
3649 UpdateMinimum(fx2f(v: Point.x), fx2f(v: Point.y), &Point);
3650 }
3651
3652 if(pMinPoint != nullptr)
3653 Ui()->SetHotItem(pMinPoint);
3654}
3655
3656void CEditor::DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function<void(ColorRGBA Color)> &SetColor)
3657{
3658 CUIRect ColorRect;
3659 pRect->Draw(Color: ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
3660 pRect->Margin(Cut: 1.0f, pOtherRect: &ColorRect);
3661 ColorRect.Draw(Color, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
3662
3663 const int ButtonResult = DoButton_Editor_Common(pId, pText: nullptr, Checked: 0, pRect, Flags: 0, pToolTip: "Click to show the color picker. Shift+rightclick to copy color to clipboard. Shift+leftclick to paste color from clipboard.");
3664 if(Input()->ShiftIsPressed())
3665 {
3666 if(ButtonResult == 1)
3667 {
3668 const char *pClipboard = Input()->GetClipboardText();
3669 if(*pClipboard == '#' || *pClipboard == '$') // ignore leading # (web color format) and $ (console color format)
3670 ++pClipboard;
3671 if(str_isallnum_hex(str: pClipboard))
3672 {
3673 std::optional<ColorRGBA> ParsedColor = color_parse<ColorRGBA>(pStr: pClipboard);
3674 if(ParsedColor)
3675 {
3676 m_ColorPickerPopupContext.m_State = EEditState::ONE_GO;
3677 SetColor(ParsedColor.value());
3678 }
3679 }
3680 }
3681 else if(ButtonResult == 2)
3682 {
3683 char aClipboard[9];
3684 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", Color.PackAlphaLast());
3685 Input()->SetClipboardText(aClipboard);
3686 }
3687 }
3688 else if(ButtonResult > 0)
3689 {
3690 if(m_ColorPickerPopupContext.m_ColorMode == CUi::SColorPickerPopupContext::MODE_UNSET)
3691 m_ColorPickerPopupContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_RGBA;
3692 m_ColorPickerPopupContext.m_RgbaColor = Color;
3693 m_ColorPickerPopupContext.m_HslaColor = color_cast<ColorHSLA>(rgb: Color);
3694 m_ColorPickerPopupContext.m_HsvaColor = color_cast<ColorHSVA>(hsl: m_ColorPickerPopupContext.m_HslaColor);
3695 m_ColorPickerPopupContext.m_Alpha = true;
3696 m_pColorPickerPopupActiveId = pId;
3697 Ui()->ShowPopupColorPicker(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &m_ColorPickerPopupContext);
3698 }
3699
3700 if(Ui()->IsPopupOpen(pId: &m_ColorPickerPopupContext))
3701 {
3702 if(m_pColorPickerPopupActiveId == pId)
3703 SetColor(m_ColorPickerPopupContext.m_RgbaColor);
3704 }
3705 else
3706 {
3707 m_pColorPickerPopupActiveId = nullptr;
3708 if(m_ColorPickerPopupContext.m_State == EEditState::EDITING)
3709 {
3710 ColorRGBA c = color_cast<ColorRGBA>(hsv: m_ColorPickerPopupContext.m_HsvaColor);
3711 m_ColorPickerPopupContext.m_State = EEditState::END;
3712 SetColor(c);
3713 m_ColorPickerPopupContext.m_State = EEditState::NONE;
3714 }
3715 }
3716}
3717
3718void CEditor::RenderLayers(CUIRect LayersBox)
3719{
3720 const float RowHeight = 12.0f;
3721 char aBuf[64];
3722
3723 CUIRect UnscrolledLayersBox = LayersBox;
3724
3725 static CScrollRegion s_ScrollRegion;
3726 vec2 ScrollOffset(0.0f, 0.0f);
3727 CScrollRegionParams ScrollParams;
3728 ScrollParams.m_ScrollbarWidth = 10.0f;
3729 ScrollParams.m_ScrollbarMargin = 3.0f;
3730 ScrollParams.m_ScrollUnit = RowHeight * 5.0f;
3731 s_ScrollRegion.Begin(pClipRect: &LayersBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
3732 LayersBox.y += ScrollOffset.y;
3733
3734 enum
3735 {
3736 OP_NONE = 0,
3737 OP_CLICK,
3738 OP_LAYER_DRAG,
3739 OP_GROUP_DRAG
3740 };
3741 static int s_Operation = OP_NONE;
3742 static int s_PreviousOperation = OP_NONE;
3743 static const void *s_pDraggedButton = nullptr;
3744 static float s_InitialMouseY = 0;
3745 static float s_InitialCutHeight = 0;
3746 constexpr float MinDragDistance = 5.0f;
3747 int GroupAfterDraggedLayer = -1;
3748 int LayerAfterDraggedLayer = -1;
3749 bool DraggedPositionFound = false;
3750 bool MoveLayers = false;
3751 bool MoveGroup = false;
3752 bool StartDragLayer = false;
3753 bool StartDragGroup = false;
3754 std::vector<int> vButtonsPerGroup;
3755
3756 auto SetOperation = [](int Operation) {
3757 if(Operation != s_Operation)
3758 {
3759 s_PreviousOperation = s_Operation;
3760 s_Operation = Operation;
3761 if(Operation == OP_NONE)
3762 {
3763 s_pDraggedButton = nullptr;
3764 }
3765 }
3766 };
3767
3768 vButtonsPerGroup.reserve(n: m_Map.m_vpGroups.size());
3769 for(const std::shared_ptr<CLayerGroup> &pGroup : m_Map.m_vpGroups)
3770 {
3771 vButtonsPerGroup.push_back(x: pGroup->m_vpLayers.size() + 1);
3772 }
3773
3774 if(s_pDraggedButton != nullptr && Ui()->ActiveItem() != s_pDraggedButton)
3775 {
3776 SetOperation(OP_NONE);
3777 }
3778
3779 if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG)
3780 {
3781 float MinDraggableValue = UnscrolledLayersBox.y;
3782 float MaxDraggableValue = MinDraggableValue;
3783 for(int NumButtons : vButtonsPerGroup)
3784 {
3785 MaxDraggableValue += NumButtons * (RowHeight + 2.0f) + 5.0f;
3786 }
3787 MaxDraggableValue += ScrollOffset.y;
3788
3789 if(s_Operation == OP_GROUP_DRAG)
3790 {
3791 MaxDraggableValue -= vButtonsPerGroup[m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f;
3792 }
3793 else if(s_Operation == OP_LAYER_DRAG)
3794 {
3795 MinDraggableValue += RowHeight + 2.0f;
3796 MaxDraggableValue -= m_vSelectedLayers.size() * (RowHeight + 2.0f) + 5.0f;
3797 }
3798
3799 UnscrolledLayersBox.HSplitTop(Cut: s_InitialCutHeight, pTop: nullptr, pBottom: &UnscrolledLayersBox);
3800 UnscrolledLayersBox.y -= s_InitialMouseY - Ui()->MouseY();
3801
3802 UnscrolledLayersBox.y = clamp(val: UnscrolledLayersBox.y, lo: MinDraggableValue, hi: MaxDraggableValue);
3803
3804 UnscrolledLayersBox.w = LayersBox.w;
3805 }
3806
3807 static bool s_ScrollToSelectionNext = false;
3808 const bool ScrollToSelection = LayerSelector()->SelectByTile() || s_ScrollToSelectionNext;
3809 s_ScrollToSelectionNext = false;
3810
3811 // render layers
3812 for(int g = 0; g < (int)m_Map.m_vpGroups.size(); g++)
3813 {
3814 if(s_Operation == OP_LAYER_DRAG && g > 0 && !DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
3815 {
3816 DraggedPositionFound = true;
3817 GroupAfterDraggedLayer = g;
3818
3819 LayerAfterDraggedLayer = m_Map.m_vpGroups[g - 1]->m_vpLayers.size();
3820
3821 CUIRect Slot;
3822 LayersBox.HSplitTop(Cut: m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &Slot, pBottom: &LayersBox);
3823 s_ScrollRegion.AddRect(Rect: Slot);
3824 }
3825
3826 CUIRect Slot, VisibleToggle;
3827 if(s_Operation == OP_GROUP_DRAG)
3828 {
3829 if(g == m_SelectedGroup)
3830 {
3831 UnscrolledLayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3832 UnscrolledLayersBox.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &UnscrolledLayersBox);
3833 }
3834 else if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f)
3835 {
3836 DraggedPositionFound = true;
3837 GroupAfterDraggedLayer = g;
3838
3839 CUIRect TmpSlot;
3840 if(m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse)
3841 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3842 else
3843 LayersBox.HSplitTop(Cut: vButtonsPerGroup[m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
3844 s_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
3845 }
3846 }
3847 if(s_Operation != OP_GROUP_DRAG || g != m_SelectedGroup)
3848 {
3849 LayersBox.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &LayersBox);
3850
3851 CUIRect TmpRect;
3852 LayersBox.HSplitTop(Cut: 2.0f, pTop: &TmpRect, pBottom: &LayersBox);
3853 s_ScrollRegion.AddRect(Rect: TmpRect);
3854 }
3855
3856 if(s_ScrollRegion.AddRect(Rect: Slot))
3857 {
3858 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Slot);
3859 if(DoButton_FontIcon(pId: &m_Map.m_vpGroups[g]->m_Visible, pText: m_Map.m_vpGroups[g]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, Checked: m_Map.m_vpGroups[g]->m_Collapse ? 1 : 0, pRect: &VisibleToggle, Flags: 0, pToolTip: "Toggle group visibility", Corners: IGraphics::CORNER_L, FontSize: 8.0f))
3860 m_Map.m_vpGroups[g]->m_Visible = !m_Map.m_vpGroups[g]->m_Visible;
3861
3862 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "#%d %s", g, m_Map.m_vpGroups[g]->m_aName);
3863
3864 bool Clicked;
3865 bool Abrupted;
3866 if(int Result = DoButton_DraggableEx(pId: m_Map.m_vpGroups[g].get(), pText: aBuf, Checked: g == m_SelectedGroup, pRect: &Slot, pClicked: &Clicked, pAbrupted: &Abrupted,
3867 Flags: BUTTON_CONTEXT, pToolTip: m_Map.m_vpGroups[g]->m_Collapse ? "Select group. Shift click to select all layers. Double click to expand." : "Select group. Shift click to select all layers. Double click to collapse.", Corners: IGraphics::CORNER_R))
3868 {
3869 if(s_Operation == OP_NONE)
3870 {
3871 s_InitialMouseY = Ui()->MouseY();
3872 s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y;
3873 SetOperation(OP_CLICK);
3874
3875 if(g != m_SelectedGroup)
3876 SelectLayer(LayerIndex: 0, GroupIndex: g);
3877 }
3878
3879 if(Abrupted)
3880 {
3881 SetOperation(OP_NONE);
3882 }
3883
3884 if(s_Operation == OP_CLICK && absolute(a: Ui()->MouseY() - s_InitialMouseY) > MinDragDistance)
3885 {
3886 StartDragGroup = true;
3887 s_pDraggedButton = m_Map.m_vpGroups[g].get();
3888 }
3889
3890 if(s_Operation == OP_CLICK && Clicked)
3891 {
3892 if(g != m_SelectedGroup)
3893 SelectLayer(LayerIndex: 0, GroupIndex: g);
3894
3895 if(Input()->ShiftIsPressed() && m_SelectedGroup == g)
3896 {
3897 m_vSelectedLayers.clear();
3898 for(size_t i = 0; i < m_Map.m_vpGroups[g]->m_vpLayers.size(); i++)
3899 {
3900 AddSelectedLayer(LayerIndex: i);
3901 }
3902 }
3903
3904 if(Result == 2)
3905 {
3906 static SPopupMenuId s_PopupGroupId;
3907 Ui()->DoPopupMenu(pId: &s_PopupGroupId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 145, Height: 256, pContext: this, pfnFunc: PopupGroup);
3908 }
3909
3910 if(!m_Map.m_vpGroups[g]->m_vpLayers.empty() && Ui()->DoDoubleClickLogic(pId: m_Map.m_vpGroups[g].get()))
3911 m_Map.m_vpGroups[g]->m_Collapse ^= 1;
3912
3913 SetOperation(OP_NONE);
3914 }
3915
3916 if(s_Operation == OP_GROUP_DRAG && Clicked)
3917 MoveGroup = true;
3918 }
3919 else if(s_pDraggedButton == m_Map.m_vpGroups[g].get())
3920 {
3921 SetOperation(OP_NONE);
3922 }
3923 }
3924
3925 for(int i = 0; i < (int)m_Map.m_vpGroups[g]->m_vpLayers.size(); i++)
3926 {
3927 if(m_Map.m_vpGroups[g]->m_Collapse)
3928 continue;
3929
3930 bool IsLayerSelected = false;
3931 if(m_SelectedGroup == g)
3932 {
3933 for(const auto &Selected : m_vSelectedLayers)
3934 {
3935 if(Selected == i)
3936 {
3937 IsLayerSelected = true;
3938 break;
3939 }
3940 }
3941 }
3942
3943 if(s_Operation == OP_GROUP_DRAG && g == m_SelectedGroup)
3944 {
3945 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3946 }
3947 else if(s_Operation == OP_LAYER_DRAG)
3948 {
3949 if(IsLayerSelected)
3950 {
3951 UnscrolledLayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &UnscrolledLayersBox);
3952 }
3953 else
3954 {
3955 if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2)
3956 {
3957 DraggedPositionFound = true;
3958 GroupAfterDraggedLayer = g + 1;
3959 LayerAfterDraggedLayer = i;
3960 for(size_t j = 0; j < m_vSelectedLayers.size(); j++)
3961 {
3962 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: nullptr, pBottom: &LayersBox);
3963 s_ScrollRegion.AddRect(Rect: Slot);
3964 }
3965 }
3966 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
3967 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
3968 continue;
3969 }
3970 }
3971 else
3972 {
3973 LayersBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &LayersBox);
3974 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: ScrollToSelection && IsLayerSelected))
3975 continue;
3976 }
3977
3978 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
3979
3980 CUIRect Button;
3981 Slot.VSplitLeft(Cut: 12.0f, pLeft: nullptr, pRight: &Slot);
3982 Slot.VSplitLeft(Cut: 15.0f, pLeft: &VisibleToggle, pRight: &Button);
3983
3984 if(DoButton_FontIcon(pId: &m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible, pText: m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, Checked: 0, pRect: &VisibleToggle, Flags: 0, pToolTip: "Toggle layer visibility", Corners: IGraphics::CORNER_L, FontSize: 8.0f))
3985 m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible = !m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible;
3986
3987 if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName[0])
3988 str_copy(dst&: aBuf, src: m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName);
3989 else
3990 {
3991 if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES)
3992 {
3993 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: m_Map.m_vpGroups[g]->m_vpLayers[i]);
3994 str_copy(dst&: aBuf, src: pTiles->m_Image >= 0 ? m_Map.m_vpImages[pTiles->m_Image]->m_aName : "Tiles");
3995 }
3996 else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS)
3997 {
3998 std::shared_ptr<CLayerQuads> pQuads = std::static_pointer_cast<CLayerQuads>(r: m_Map.m_vpGroups[g]->m_vpLayers[i]);
3999 str_copy(dst&: aBuf, src: pQuads->m_Image >= 0 ? m_Map.m_vpImages[pQuads->m_Image]->m_aName : "Quads");
4000 }
4001 else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS)
4002 {
4003 std::shared_ptr<CLayerSounds> pSounds = std::static_pointer_cast<CLayerSounds>(r: m_Map.m_vpGroups[g]->m_vpLayers[i]);
4004 str_copy(dst&: aBuf, src: pSounds->m_Sound >= 0 ? m_Map.m_vpSounds[pSounds->m_Sound]->m_aName : "Sounds");
4005 }
4006 }
4007
4008 int Checked = IsLayerSelected ? 1 : 0;
4009 if(m_Map.m_vpGroups[g]->m_vpLayers[i]->IsEntitiesLayer())
4010 {
4011 Checked += 6;
4012 }
4013
4014 bool Clicked;
4015 bool Abrupted;
4016 if(int Result = DoButton_DraggableEx(pId: m_Map.m_vpGroups[g]->m_vpLayers[i].get(), pText: aBuf, Checked, pRect: &Button, pClicked: &Clicked, pAbrupted: &Abrupted,
4017 Flags: BUTTON_CONTEXT, pToolTip: "Select layer. Shift click to select multiple.", Corners: IGraphics::CORNER_R))
4018 {
4019 if(s_Operation == OP_NONE)
4020 {
4021 s_InitialMouseY = Ui()->MouseY();
4022 s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y;
4023
4024 SetOperation(OP_CLICK);
4025
4026 if(!Input()->ShiftIsPressed() && !IsLayerSelected)
4027 {
4028 SelectLayer(LayerIndex: i, GroupIndex: g);
4029 }
4030 }
4031
4032 if(Abrupted)
4033 {
4034 SetOperation(OP_NONE);
4035 }
4036
4037 if(s_Operation == OP_CLICK && absolute(a: Ui()->MouseY() - s_InitialMouseY) > MinDragDistance)
4038 {
4039 bool EntitiesLayerSelected = false;
4040 for(int k : m_vSelectedLayers)
4041 {
4042 if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer())
4043 EntitiesLayerSelected = true;
4044 }
4045
4046 if(!EntitiesLayerSelected)
4047 StartDragLayer = true;
4048
4049 s_pDraggedButton = m_Map.m_vpGroups[g]->m_vpLayers[i].get();
4050 }
4051
4052 if(s_Operation == OP_CLICK && Clicked)
4053 {
4054 static SLayerPopupContext s_LayerPopupContext = {};
4055 s_LayerPopupContext.m_pEditor = this;
4056 if(Result == 1)
4057 {
4058 if(Input()->ShiftIsPressed() && m_SelectedGroup == g)
4059 {
4060 auto Position = std::find(first: m_vSelectedLayers.begin(), last: m_vSelectedLayers.end(), val: i);
4061 if(Position != m_vSelectedLayers.end())
4062 m_vSelectedLayers.erase(position: Position);
4063 else
4064 AddSelectedLayer(LayerIndex: i);
4065 }
4066 else if(!Input()->ShiftIsPressed())
4067 {
4068 SelectLayer(LayerIndex: i, GroupIndex: g);
4069 }
4070 }
4071 else if(Result == 2)
4072 {
4073 s_LayerPopupContext.m_vpLayers.clear();
4074 s_LayerPopupContext.m_vLayerIndices.clear();
4075
4076 if(!IsLayerSelected)
4077 {
4078 SelectLayer(LayerIndex: i, GroupIndex: g);
4079 }
4080
4081 if(m_vSelectedLayers.size() > 1)
4082 {
4083 bool AllTile = true;
4084 for(size_t j = 0; AllTile && j < m_vSelectedLayers.size(); j++)
4085 {
4086 int LayerIndex = m_vSelectedLayers[j];
4087 if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES)
4088 {
4089 s_LayerPopupContext.m_vpLayers.push_back(x: std::static_pointer_cast<CLayerTiles>(r: m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[m_vSelectedLayers[j]]));
4090 s_LayerPopupContext.m_vLayerIndices.push_back(x: LayerIndex);
4091 }
4092 else
4093 AllTile = false;
4094 }
4095
4096 // Don't allow editing if all selected layers are not tile layers
4097 if(!AllTile)
4098 {
4099 s_LayerPopupContext.m_vpLayers.clear();
4100 s_LayerPopupContext.m_vLayerIndices.clear();
4101 }
4102 }
4103
4104 Ui()->DoPopupMenu(pId: &s_LayerPopupContext, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 120, Height: 270, pContext: &s_LayerPopupContext, pfnFunc: PopupLayer);
4105 }
4106
4107 SetOperation(OP_NONE);
4108 }
4109
4110 if(s_Operation == OP_LAYER_DRAG && Clicked)
4111 {
4112 MoveLayers = true;
4113 }
4114 }
4115 else if(s_pDraggedButton == m_Map.m_vpGroups[g]->m_vpLayers[i].get())
4116 {
4117 SetOperation(OP_NONE);
4118 }
4119 }
4120
4121 if(s_Operation != OP_GROUP_DRAG || g != m_SelectedGroup)
4122 {
4123 LayersBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &LayersBox);
4124 s_ScrollRegion.AddRect(Rect: Slot);
4125 }
4126 }
4127
4128 if(!DraggedPositionFound && s_Operation == OP_LAYER_DRAG)
4129 {
4130 GroupAfterDraggedLayer = m_Map.m_vpGroups.size();
4131 LayerAfterDraggedLayer = m_Map.m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers.size();
4132
4133 CUIRect TmpSlot;
4134 LayersBox.HSplitTop(Cut: m_vSelectedLayers.size() * (RowHeight + 2.0f), pTop: &TmpSlot, pBottom: &LayersBox);
4135 s_ScrollRegion.AddRect(Rect: TmpSlot);
4136 }
4137
4138 if(!DraggedPositionFound && s_Operation == OP_GROUP_DRAG)
4139 {
4140 GroupAfterDraggedLayer = m_Map.m_vpGroups.size();
4141
4142 CUIRect TmpSlot;
4143 if(m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse)
4144 LayersBox.HSplitTop(Cut: RowHeight + 7.0f, pTop: &TmpSlot, pBottom: &LayersBox);
4145 else
4146 LayersBox.HSplitTop(Cut: vButtonsPerGroup[m_SelectedGroup] * (RowHeight + 2.0f) + 5.0f, pTop: &TmpSlot, pBottom: &LayersBox);
4147 s_ScrollRegion.AddRect(Rect: TmpSlot, ShouldScrollHere: false);
4148 }
4149
4150 if(MoveLayers && 1 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)m_Map.m_vpGroups.size())
4151 {
4152 std::vector<std::shared_ptr<CLayer>> &vpNewGroupLayers = m_Map.m_vpGroups[GroupAfterDraggedLayer - 1]->m_vpLayers;
4153 if(0 <= LayerAfterDraggedLayer && LayerAfterDraggedLayer <= (int)vpNewGroupLayers.size())
4154 {
4155 std::vector<std::shared_ptr<CLayer>> vpSelectedLayers;
4156 std::vector<std::shared_ptr<CLayer>> &vpSelectedGroupLayers = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers;
4157 std::shared_ptr<CLayer> pNextLayer = nullptr;
4158 if(LayerAfterDraggedLayer < (int)vpNewGroupLayers.size())
4159 pNextLayer = vpNewGroupLayers[LayerAfterDraggedLayer];
4160
4161 std::sort(first: m_vSelectedLayers.begin(), last: m_vSelectedLayers.end(), comp: std::greater<>());
4162 for(int k : m_vSelectedLayers)
4163 {
4164 vpSelectedLayers.insert(position: vpSelectedLayers.begin(), x: vpSelectedGroupLayers[k]);
4165 }
4166 for(int k : m_vSelectedLayers)
4167 {
4168 vpSelectedGroupLayers.erase(position: vpSelectedGroupLayers.begin() + k);
4169 }
4170
4171 auto InsertPosition = std::find(first: vpNewGroupLayers.begin(), last: vpNewGroupLayers.end(), val: pNextLayer);
4172 int InsertPositionIndex = InsertPosition - vpNewGroupLayers.begin();
4173 vpNewGroupLayers.insert(position: InsertPosition, first: vpSelectedLayers.begin(), last: vpSelectedLayers.end());
4174
4175 int NumSelectedLayers = m_vSelectedLayers.size();
4176 m_vSelectedLayers.clear();
4177 for(int i = 0; i < NumSelectedLayers; i++)
4178 m_vSelectedLayers.push_back(x: InsertPositionIndex + i);
4179
4180 m_SelectedGroup = GroupAfterDraggedLayer - 1;
4181 m_Map.OnModify();
4182 }
4183 }
4184
4185 if(MoveGroup && 0 <= GroupAfterDraggedLayer && GroupAfterDraggedLayer <= (int)m_Map.m_vpGroups.size())
4186 {
4187 std::shared_ptr<CLayerGroup> pSelectedGroup = m_Map.m_vpGroups[m_SelectedGroup];
4188 std::shared_ptr<CLayerGroup> pNextGroup = nullptr;
4189 if(GroupAfterDraggedLayer < (int)m_Map.m_vpGroups.size())
4190 pNextGroup = m_Map.m_vpGroups[GroupAfterDraggedLayer];
4191
4192 m_Map.m_vpGroups.erase(position: m_Map.m_vpGroups.begin() + m_SelectedGroup);
4193
4194 auto InsertPosition = std::find(first: m_Map.m_vpGroups.begin(), last: m_Map.m_vpGroups.end(), val: pNextGroup);
4195 m_Map.m_vpGroups.insert(position: InsertPosition, x: pSelectedGroup);
4196
4197 auto Pos = std::find(first: m_Map.m_vpGroups.begin(), last: m_Map.m_vpGroups.end(), val: pSelectedGroup);
4198 m_SelectedGroup = Pos - m_Map.m_vpGroups.begin();
4199
4200 m_Map.OnModify();
4201 }
4202
4203 static int s_InitialGroupIndex;
4204 static std::vector<int> s_vInitialLayerIndices;
4205
4206 if(MoveLayers || MoveGroup)
4207 {
4208 SetOperation(OP_NONE);
4209 }
4210 if(StartDragLayer)
4211 {
4212 SetOperation(OP_LAYER_DRAG);
4213 s_InitialGroupIndex = m_SelectedGroup;
4214 s_vInitialLayerIndices = std::vector(m_vSelectedLayers);
4215 }
4216 if(StartDragGroup)
4217 {
4218 s_InitialGroupIndex = m_SelectedGroup;
4219 SetOperation(OP_GROUP_DRAG);
4220 }
4221
4222 if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG)
4223 {
4224 if(s_pDraggedButton == nullptr)
4225 {
4226 SetOperation(OP_NONE);
4227 }
4228 else
4229 {
4230 s_ScrollRegion.DoEdgeScrolling();
4231 Ui()->SetActiveItem(s_pDraggedButton);
4232 }
4233 }
4234
4235 if(Input()->KeyPress(Key: KEY_DOWN) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE)
4236 {
4237 if(Input()->ShiftIsPressed())
4238 {
4239 if(m_vSelectedLayers[m_vSelectedLayers.size() - 1] < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1)
4240 AddSelectedLayer(LayerIndex: m_vSelectedLayers[m_vSelectedLayers.size() - 1] + 1);
4241 }
4242 else
4243 {
4244 int CurrentLayer = 0;
4245 for(const auto &Selected : m_vSelectedLayers)
4246 CurrentLayer = maximum(a: Selected, b: CurrentLayer);
4247 SelectLayer(LayerIndex: CurrentLayer);
4248
4249 if(m_vSelectedLayers[0] < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1)
4250 {
4251 SelectLayer(LayerIndex: m_vSelectedLayers[0] + 1);
4252 }
4253 else
4254 {
4255 for(size_t Group = m_SelectedGroup + 1; Group < m_Map.m_vpGroups.size(); Group++)
4256 {
4257 if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty())
4258 {
4259 SelectLayer(LayerIndex: 0, GroupIndex: Group);
4260 break;
4261 }
4262 }
4263 }
4264 }
4265 s_ScrollToSelectionNext = true;
4266 }
4267 if(Input()->KeyPress(Key: KEY_UP) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE)
4268 {
4269 if(Input()->ShiftIsPressed())
4270 {
4271 if(m_vSelectedLayers[m_vSelectedLayers.size() - 1] > 0)
4272 AddSelectedLayer(LayerIndex: m_vSelectedLayers[m_vSelectedLayers.size() - 1] - 1);
4273 }
4274 else
4275 {
4276 int CurrentLayer = std::numeric_limits<int>::max();
4277 for(const auto &Selected : m_vSelectedLayers)
4278 CurrentLayer = minimum(a: Selected, b: CurrentLayer);
4279 SelectLayer(LayerIndex: CurrentLayer);
4280
4281 if(m_vSelectedLayers[0] > 0)
4282 {
4283 SelectLayer(LayerIndex: m_vSelectedLayers[0] - 1);
4284 }
4285 else
4286 {
4287 for(int Group = m_SelectedGroup - 1; Group >= 0; Group--)
4288 {
4289 if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty())
4290 {
4291 SelectLayer(LayerIndex: m_Map.m_vpGroups[Group]->m_vpLayers.size() - 1, GroupIndex: Group);
4292 break;
4293 }
4294 }
4295 }
4296 }
4297 s_ScrollToSelectionNext = true;
4298 }
4299
4300 CUIRect AddGroupButton;
4301 LayersBox.HSplitTop(Cut: RowHeight + 1.0f, pTop: &AddGroupButton, pBottom: &LayersBox);
4302 if(s_ScrollRegion.AddRect(Rect: AddGroupButton))
4303 {
4304 AddGroupButton.HSplitTop(Cut: RowHeight, pTop: &AddGroupButton, pBottom: 0);
4305 static int s_AddGroupButton = 0;
4306 if(DoButton_Editor(pId: &s_AddGroupButton, pText: "Add group", Checked: 0, pRect: &AddGroupButton, Flags: IGraphics::CORNER_R, pToolTip: "Adds a new group"))
4307 {
4308 m_Map.NewGroup();
4309 m_SelectedGroup = m_Map.m_vpGroups.size() - 1;
4310 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionGroup>(args: this, args&: m_SelectedGroup, args: false));
4311 }
4312 }
4313
4314 s_ScrollRegion.End();
4315
4316 if(s_Operation == OP_NONE)
4317 {
4318 if(s_PreviousOperation == OP_GROUP_DRAG)
4319 {
4320 s_PreviousOperation = OP_NONE;
4321 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditGroupProp>(args: this, args&: m_SelectedGroup, args: EGroupProp::PROP_ORDER, args&: s_InitialGroupIndex, args&: m_SelectedGroup));
4322 }
4323 else if(s_PreviousOperation == OP_LAYER_DRAG)
4324 {
4325 if(s_InitialGroupIndex != m_SelectedGroup)
4326 {
4327 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEditLayersGroupAndOrder>(args: this, args&: s_InitialGroupIndex, args&: s_vInitialLayerIndices, args&: m_SelectedGroup, args&: m_vSelectedLayers));
4328 }
4329 else
4330 {
4331 std::vector<std::shared_ptr<IEditorAction>> vpActions;
4332 std::vector<int> vLayerIndices = m_vSelectedLayers;
4333 std::sort(first: vLayerIndices.begin(), last: vLayerIndices.end());
4334 std::sort(first: s_vInitialLayerIndices.begin(), last: s_vInitialLayerIndices.end());
4335 for(int k = 0; k < (int)vLayerIndices.size(); k++)
4336 {
4337 int LayerIndex = vLayerIndices[k];
4338 vpActions.push_back(x: std::make_shared<CEditorActionEditLayerProp>(args: this, args&: m_SelectedGroup, args&: LayerIndex, args: ELayerProp::PROP_ORDER, args&: s_vInitialLayerIndices[k], args&: LayerIndex));
4339 }
4340 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: this, args&: vpActions, args: nullptr, args: true));
4341 }
4342 s_PreviousOperation = OP_NONE;
4343 }
4344 }
4345}
4346
4347bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDuplicate)
4348{
4349 // check if we have that image already
4350 char aBuf[128];
4351 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4352 if(CheckDuplicate)
4353 {
4354 for(const auto &pImage : m_Map.m_vpImages)
4355 {
4356 if(!str_comp(a: pImage->m_aName, b: aBuf))
4357 {
4358 ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
4359 return false;
4360 }
4361 }
4362 }
4363
4364 CEditorImage ImgInfo(this);
4365 if(!Graphics()->LoadPng(Image&: ImgInfo, pFilename: pFileName, StorageType))
4366 {
4367 ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFileName);
4368 return false;
4369 }
4370
4371 std::shared_ptr<CEditorImage> pImg = m_Map.m_vpImages[m_SelectedImage];
4372 Graphics()->UnloadTexture(pIndex: &(pImg->m_Texture));
4373 pImg->Free();
4374 *pImg = ImgInfo;
4375 str_copy(dst&: pImg->m_aName, src: aBuf);
4376 pImg->m_External = IsVanillaImage(pImage: pImg->m_aName);
4377
4378 if(!pImg->m_External && g_Config.m_ClEditorDilate == 1 && pImg->m_Format == CImageInfo::FORMAT_RGBA)
4379 {
4380 DilateImage(pImageBuff: ImgInfo.m_pData, w: ImgInfo.m_Width, h: ImgInfo.m_Height);
4381 }
4382
4383 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
4384 int TextureLoadFlag = Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
4385 if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0)
4386 TextureLoadFlag = 0;
4387 pImg->m_Texture = Graphics()->LoadTextureRaw(Image: ImgInfo, Flags: TextureLoadFlag, pTexName: pFileName);
4388 ImgInfo.m_pData = nullptr;
4389 SortImages();
4390 for(size_t i = 0; i < m_Map.m_vpImages.size(); ++i)
4391 {
4392 if(!str_comp(a: m_Map.m_vpImages[i]->m_aName, b: pImg->m_aName))
4393 m_SelectedImage = i;
4394 }
4395 m_Dialog = DIALOG_NONE;
4396 return true;
4397}
4398
4399bool CEditor::ReplaceImageCallback(const char *pFileName, int StorageType, void *pUser)
4400{
4401 return static_cast<CEditor *>(pUser)->ReplaceImage(pFileName, StorageType, CheckDuplicate: true);
4402}
4403
4404bool CEditor::AddImage(const char *pFileName, int StorageType, void *pUser)
4405{
4406 CEditor *pEditor = (CEditor *)pUser;
4407
4408 // check if we have that image already
4409 char aBuf[128];
4410 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4411 for(const auto &pImage : pEditor->m_Map.m_vpImages)
4412 {
4413 if(!str_comp(a: pImage->m_aName, b: aBuf))
4414 {
4415 pEditor->ShowFileDialogError(pFormat: "Image named '%s' was already added.", pImage->m_aName);
4416 return false;
4417 }
4418 }
4419
4420 if(pEditor->m_Map.m_vpImages.size() >= MAX_MAPIMAGES)
4421 {
4422 pEditor->m_PopupEventType = POPEVENT_IMAGE_MAX;
4423 pEditor->m_PopupEventActivated = true;
4424 return false;
4425 }
4426
4427 CEditorImage ImgInfo(pEditor);
4428 if(!pEditor->Graphics()->LoadPng(Image&: ImgInfo, pFilename: pFileName, StorageType))
4429 {
4430 pEditor->ShowFileDialogError(pFormat: "Failed to load image from file '%s'.", pFileName);
4431 return false;
4432 }
4433
4434 std::shared_ptr<CEditorImage> pImg = std::make_shared<CEditorImage>(args&: pEditor);
4435 *pImg = ImgInfo;
4436 pImg->m_External = IsVanillaImage(pImage: aBuf);
4437
4438 if(!pImg->m_External && g_Config.m_ClEditorDilate == 1 && pImg->m_Format == CImageInfo::FORMAT_RGBA)
4439 {
4440 DilateImage(pImageBuff: ImgInfo.m_pData, w: ImgInfo.m_Width, h: ImgInfo.m_Height);
4441 }
4442
4443 int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
4444 if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0)
4445 TextureLoadFlag = 0;
4446 pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image: ImgInfo, Flags: TextureLoadFlag, pTexName: pFileName);
4447 ImgInfo.m_pData = nullptr;
4448 str_copy(dst&: pImg->m_aName, src: aBuf);
4449 pImg->m_AutoMapper.Load(pTileName: pImg->m_aName);
4450 pEditor->m_Map.m_vpImages.push_back(x: pImg);
4451 pEditor->SortImages();
4452 if(pEditor->m_SelectedImage >= 0 && (size_t)pEditor->m_SelectedImage < pEditor->m_Map.m_vpImages.size())
4453 {
4454 for(int i = 0; i <= pEditor->m_SelectedImage; ++i)
4455 if(!str_comp(a: pEditor->m_Map.m_vpImages[i]->m_aName, b: aBuf))
4456 {
4457 pEditor->m_SelectedImage++;
4458 break;
4459 }
4460 }
4461 pEditor->m_Dialog = DIALOG_NONE;
4462 return true;
4463}
4464
4465bool CEditor::AddSound(const char *pFileName, int StorageType, void *pUser)
4466{
4467 CEditor *pEditor = (CEditor *)pUser;
4468
4469 // check if we have that sound already
4470 char aBuf[128];
4471 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4472 for(const auto &pSound : pEditor->m_Map.m_vpSounds)
4473 {
4474 if(!str_comp(a: pSound->m_aName, b: aBuf))
4475 {
4476 pEditor->ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
4477 return false;
4478 }
4479 }
4480
4481 if(pEditor->m_Map.m_vpSounds.size() >= MAX_MAPSOUNDS)
4482 {
4483 pEditor->m_PopupEventType = POPEVENT_SOUND_MAX;
4484 pEditor->m_PopupEventActivated = true;
4485 return false;
4486 }
4487
4488 // load external
4489 void *pData;
4490 unsigned DataSize;
4491 if(!pEditor->Storage()->ReadFile(pFilename: pFileName, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
4492 {
4493 pEditor->ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFileName);
4494 return false;
4495 }
4496
4497 // load sound
4498 const int SoundId = pEditor->Sound()->LoadOpusFromMem(pData, DataSize, FromEditor: true);
4499 if(SoundId == -1)
4500 {
4501 free(ptr: pData);
4502 pEditor->ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFileName);
4503 return false;
4504 }
4505
4506 // add sound
4507 std::shared_ptr<CEditorSound> pSound = std::make_shared<CEditorSound>(args&: pEditor);
4508 pSound->m_SoundId = SoundId;
4509 pSound->m_DataSize = DataSize;
4510 pSound->m_pData = pData;
4511 str_copy(dst&: pSound->m_aName, src: aBuf);
4512 pEditor->m_Map.m_vpSounds.push_back(x: pSound);
4513
4514 if(pEditor->m_SelectedSound >= 0 && (size_t)pEditor->m_SelectedSound < pEditor->m_Map.m_vpSounds.size())
4515 {
4516 for(int i = 0; i <= pEditor->m_SelectedSound; ++i)
4517 if(!str_comp(a: pEditor->m_Map.m_vpSounds[i]->m_aName, b: aBuf))
4518 {
4519 pEditor->m_SelectedSound++;
4520 break;
4521 }
4522 }
4523
4524 pEditor->OnDialogClose();
4525 pEditor->m_Dialog = DIALOG_NONE;
4526 return true;
4527}
4528
4529bool CEditor::ReplaceSound(const char *pFileName, int StorageType, bool CheckDuplicate)
4530{
4531 // check if we have that sound already
4532 char aBuf[128];
4533 IStorage::StripPathAndExtension(pFilename: pFileName, pBuffer: aBuf, BufferSize: sizeof(aBuf));
4534 if(CheckDuplicate)
4535 {
4536 for(const auto &pSound : m_Map.m_vpSounds)
4537 {
4538 if(!str_comp(a: pSound->m_aName, b: aBuf))
4539 {
4540 ShowFileDialogError(pFormat: "Sound named '%s' was already added.", pSound->m_aName);
4541 return false;
4542 }
4543 }
4544 }
4545
4546 // load external
4547 void *pData;
4548 unsigned DataSize;
4549 if(!Storage()->ReadFile(pFilename: pFileName, Type: StorageType, ppResult: &pData, pResultLen: &DataSize))
4550 {
4551 ShowFileDialogError(pFormat: "Failed to open sound file '%s'.", pFileName);
4552 return false;
4553 }
4554
4555 // load sound
4556 const int SoundId = Sound()->LoadOpusFromMem(pData, DataSize, FromEditor: true);
4557 if(SoundId == -1)
4558 {
4559 free(ptr: pData);
4560 ShowFileDialogError(pFormat: "Failed to load sound from file '%s'.", pFileName);
4561 return false;
4562 }
4563
4564 std::shared_ptr<CEditorSound> pSound = m_Map.m_vpSounds[m_SelectedSound];
4565
4566 // unload sample
4567 Sound()->UnloadSample(SampleId: pSound->m_SoundId);
4568 free(ptr: pSound->m_pData);
4569
4570 // replace sound
4571 str_copy(dst&: pSound->m_aName, src: aBuf);
4572 pSound->m_SoundId = SoundId;
4573 pSound->m_pData = pData;
4574 pSound->m_DataSize = DataSize;
4575
4576 OnDialogClose();
4577 m_Dialog = DIALOG_NONE;
4578 return true;
4579}
4580
4581bool CEditor::ReplaceSoundCallback(const char *pFileName, int StorageType, void *pUser)
4582{
4583 return static_cast<CEditor *>(pUser)->ReplaceSound(pFileName, StorageType, CheckDuplicate: true);
4584}
4585
4586void CEditor::SelectGameLayer()
4587{
4588 for(size_t g = 0; g < m_Map.m_vpGroups.size(); g++)
4589 {
4590 for(size_t i = 0; i < m_Map.m_vpGroups[g]->m_vpLayers.size(); i++)
4591 {
4592 if(m_Map.m_vpGroups[g]->m_vpLayers[i] == m_Map.m_pGameLayer)
4593 {
4594 SelectLayer(LayerIndex: i, GroupIndex: g);
4595 return;
4596 }
4597 }
4598 }
4599}
4600
4601std::vector<int> CEditor::SortImages()
4602{
4603 static const auto &&s_ImageNameComparator = [](const std::shared_ptr<CEditorImage> &pLhs, const std::shared_ptr<CEditorImage> &pRhs) {
4604 return str_comp(a: pLhs->m_aName, b: pRhs->m_aName) < 0;
4605 };
4606 if(!std::is_sorted(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), comp: s_ImageNameComparator))
4607 {
4608 const std::vector<std::shared_ptr<CEditorImage>> vpTemp = m_Map.m_vpImages;
4609 std::vector<int> vSortedIndex;
4610 vSortedIndex.resize(new_size: vpTemp.size());
4611
4612 std::sort(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), comp: s_ImageNameComparator);
4613 for(size_t OldIndex = 0; OldIndex < vpTemp.size(); OldIndex++)
4614 {
4615 for(size_t NewIndex = 0; NewIndex < m_Map.m_vpImages.size(); NewIndex++)
4616 {
4617 if(vpTemp[OldIndex] == m_Map.m_vpImages[NewIndex])
4618 {
4619 vSortedIndex[OldIndex] = NewIndex;
4620 break;
4621 }
4622 }
4623 }
4624 m_Map.ModifyImageIndex(pfnFunc: [vSortedIndex](int *pIndex) {
4625 if(*pIndex >= 0)
4626 *pIndex = vSortedIndex[*pIndex];
4627 });
4628
4629 return vSortedIndex;
4630 }
4631
4632 return std::vector<int>();
4633}
4634
4635void CEditor::RenderImagesList(CUIRect ToolBox)
4636{
4637 const float RowHeight = 12.0f;
4638
4639 static CScrollRegion s_ScrollRegion;
4640 vec2 ScrollOffset(0.0f, 0.0f);
4641 CScrollRegionParams ScrollParams;
4642 ScrollParams.m_ScrollbarWidth = 10.0f;
4643 ScrollParams.m_ScrollbarMargin = 3.0f;
4644 ScrollParams.m_ScrollUnit = RowHeight * 5;
4645 s_ScrollRegion.Begin(pClipRect: &ToolBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
4646 ToolBox.y += ScrollOffset.y;
4647
4648 bool ScrollToSelection = false;
4649 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !m_Map.m_vpImages.empty())
4650 {
4651 if(Input()->KeyPress(Key: KEY_DOWN))
4652 {
4653 int OldImage = m_SelectedImage;
4654 m_SelectedImage = clamp(val: m_SelectedImage, lo: 0, hi: (int)m_Map.m_vpImages.size() - 1);
4655 for(size_t i = m_SelectedImage + 1; i < m_Map.m_vpImages.size(); i++)
4656 {
4657 if(m_Map.m_vpImages[i]->m_External == m_Map.m_vpImages[m_SelectedImage]->m_External)
4658 {
4659 m_SelectedImage = i;
4660 break;
4661 }
4662 }
4663 if(m_SelectedImage == OldImage && !m_Map.m_vpImages[m_SelectedImage]->m_External)
4664 {
4665 for(size_t i = 0; i < m_Map.m_vpImages.size(); i++)
4666 {
4667 if(m_Map.m_vpImages[i]->m_External)
4668 {
4669 m_SelectedImage = i;
4670 break;
4671 }
4672 }
4673 }
4674 ScrollToSelection = OldImage != m_SelectedImage;
4675 }
4676 else if(Input()->KeyPress(Key: KEY_UP))
4677 {
4678 int OldImage = m_SelectedImage;
4679 m_SelectedImage = clamp(val: m_SelectedImage, lo: 0, hi: (int)m_Map.m_vpImages.size() - 1);
4680 for(int i = m_SelectedImage - 1; i >= 0; i--)
4681 {
4682 if(m_Map.m_vpImages[i]->m_External == m_Map.m_vpImages[m_SelectedImage]->m_External)
4683 {
4684 m_SelectedImage = i;
4685 break;
4686 }
4687 }
4688 if(m_SelectedImage == OldImage && m_Map.m_vpImages[m_SelectedImage]->m_External)
4689 {
4690 for(int i = (int)m_Map.m_vpImages.size() - 1; i >= 0; i--)
4691 {
4692 if(!m_Map.m_vpImages[i]->m_External)
4693 {
4694 m_SelectedImage = i;
4695 break;
4696 }
4697 }
4698 }
4699 ScrollToSelection = OldImage != m_SelectedImage;
4700 }
4701 }
4702
4703 for(int e = 0; e < 2; e++) // two passes, first embedded, then external
4704 {
4705 CUIRect Slot;
4706 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
4707 if(s_ScrollRegion.AddRect(Rect: Slot))
4708 Ui()->DoLabel(pRect: &Slot, pText: e == 0 ? "Embedded" : "External", Size: 12.0f, Align: TEXTALIGN_MC);
4709
4710 for(int i = 0; i < (int)m_Map.m_vpImages.size(); i++)
4711 {
4712 if((e && !m_Map.m_vpImages[i]->m_External) ||
4713 (!e && m_Map.m_vpImages[i]->m_External))
4714 {
4715 continue;
4716 }
4717
4718 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
4719 int Selected = m_SelectedImage == i;
4720 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
4721 continue;
4722 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
4723
4724 const bool ImageUsed = std::any_of(first: m_Map.m_vpGroups.cbegin(), last: m_Map.m_vpGroups.cend(), pred: [i](const auto &pGroup) {
4725 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
4726 if(pLayer->m_Type == LAYERTYPE_QUADS)
4727 return std::static_pointer_cast<CLayerQuads>(pLayer)->m_Image == i;
4728 else if(pLayer->m_Type == LAYERTYPE_TILES)
4729 return std::static_pointer_cast<CLayerTiles>(pLayer)->m_Image == i;
4730 return false;
4731 });
4732 });
4733
4734 if(!ImageUsed)
4735 Selected += 2; // Image is unused
4736
4737 if(Selected < 2 && e == 1)
4738 {
4739 if(!IsVanillaImage(pImage: m_Map.m_vpImages[i]->m_aName))
4740 {
4741 Selected += 4; // Image should be embedded
4742 }
4743 }
4744
4745 if(int Result = DoButton_Ex(pId: &m_Map.m_vpImages[i], pText: m_Map.m_vpImages[i]->m_aName, Checked: Selected, pRect: &Slot,
4746 Flags: BUTTON_CONTEXT, pToolTip: "Select image.", Corners: IGraphics::CORNER_ALL))
4747 {
4748 m_SelectedImage = i;
4749
4750 if(Result == 2)
4751 {
4752 const std::shared_ptr<CEditorImage> pImg = m_Map.m_vpImages[m_SelectedImage];
4753 const int Height = pImg->m_External ? 73 : 107;
4754 static SPopupMenuId s_PopupImageId;
4755 Ui()->DoPopupMenu(pId: &s_PopupImageId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height, pContext: this, pfnFunc: PopupImage);
4756 }
4757 }
4758 }
4759
4760 // separator
4761 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
4762 if(s_ScrollRegion.AddRect(Rect: Slot))
4763 {
4764 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
4765 Graphics()->TextureClear();
4766 Graphics()->LinesBegin();
4767 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4768 Graphics()->LinesEnd();
4769 }
4770 }
4771
4772 // new image
4773 static int s_AddImageButton = 0;
4774 CUIRect AddImageButton;
4775 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddImageButton, pBottom: &ToolBox);
4776 if(s_ScrollRegion.AddRect(Rect: AddImageButton))
4777 {
4778 AddImageButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddImageButton);
4779 AddImageButton.HSplitTop(Cut: RowHeight, pTop: &AddImageButton, pBottom: nullptr);
4780 if(DoButton_Editor(pId: &s_AddImageButton, pText: "Add", Checked: 0, pRect: &AddImageButton, Flags: 0, pToolTip: "Load a new image to use in the map"))
4781 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_IMG, pTitle: "Add Image", pButtonText: "Add", pBasepath: "mapres", FilenameAsDefault: false, pfnFunc: AddImage, pUser: this);
4782 }
4783 s_ScrollRegion.End();
4784}
4785
4786void CEditor::RenderSelectedImage(CUIRect View)
4787{
4788 if(m_SelectedImage < 0 || (size_t)m_SelectedImage >= m_Map.m_vpImages.size())
4789 return;
4790
4791 View.Margin(Cut: 10.0f, pOtherRect: &View);
4792 if(View.h < View.w)
4793 View.w = View.h;
4794 else
4795 View.h = View.w;
4796 float Max = maximum<float>(a: m_Map.m_vpImages[m_SelectedImage]->m_Width, b: m_Map.m_vpImages[m_SelectedImage]->m_Height);
4797 View.w *= m_Map.m_vpImages[m_SelectedImage]->m_Width / Max;
4798 View.h *= m_Map.m_vpImages[m_SelectedImage]->m_Height / Max;
4799 Graphics()->TextureSet(Texture: m_Map.m_vpImages[m_SelectedImage]->m_Texture);
4800 Graphics()->BlendNormal();
4801 Graphics()->WrapClamp();
4802 Graphics()->QuadsBegin();
4803 IGraphics::CQuadItem QuadItem(View.x, View.y, View.w, View.h);
4804 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
4805 Graphics()->QuadsEnd();
4806 Graphics()->WrapNormal();
4807}
4808
4809void CEditor::RenderSounds(CUIRect ToolBox)
4810{
4811 const float RowHeight = 12.0f;
4812
4813 static CScrollRegion s_ScrollRegion;
4814 vec2 ScrollOffset(0.0f, 0.0f);
4815 CScrollRegionParams ScrollParams;
4816 ScrollParams.m_ScrollbarWidth = 10.0f;
4817 ScrollParams.m_ScrollbarMargin = 3.0f;
4818 ScrollParams.m_ScrollUnit = RowHeight * 5;
4819 s_ScrollRegion.Begin(pClipRect: &ToolBox, pOutOffset: &ScrollOffset, pParams: &ScrollParams);
4820 ToolBox.y += ScrollOffset.y;
4821
4822 bool ScrollToSelection = false;
4823 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && !m_Map.m_vpSounds.empty())
4824 {
4825 if(Input()->KeyPress(Key: KEY_DOWN))
4826 {
4827 m_SelectedSound = (m_SelectedSound + 1) % m_Map.m_vpSounds.size();
4828 ScrollToSelection = true;
4829 }
4830 else if(Input()->KeyPress(Key: KEY_UP))
4831 {
4832 m_SelectedSound = (m_SelectedSound + m_Map.m_vpSounds.size() - 1) % m_Map.m_vpSounds.size();
4833 ScrollToSelection = true;
4834 }
4835 }
4836
4837 CUIRect Slot;
4838 ToolBox.HSplitTop(Cut: RowHeight + 3.0f, pTop: &Slot, pBottom: &ToolBox);
4839 if(s_ScrollRegion.AddRect(Rect: Slot))
4840 Ui()->DoLabel(pRect: &Slot, pText: "Embedded", Size: 12.0f, Align: TEXTALIGN_MC);
4841
4842 for(int i = 0; i < (int)m_Map.m_vpSounds.size(); i++)
4843 {
4844 ToolBox.HSplitTop(Cut: RowHeight + 2.0f, pTop: &Slot, pBottom: &ToolBox);
4845 int Selected = m_SelectedSound == i;
4846 if(!s_ScrollRegion.AddRect(Rect: Slot, ShouldScrollHere: Selected && ScrollToSelection))
4847 continue;
4848 Slot.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: nullptr);
4849
4850 const bool SoundUsed = std::any_of(first: m_Map.m_vpGroups.cbegin(), last: m_Map.m_vpGroups.cend(), pred: [i](const auto &pGroup) {
4851 return std::any_of(pGroup->m_vpLayers.cbegin(), pGroup->m_vpLayers.cend(), [i](const auto &pLayer) {
4852 if(pLayer->m_Type == LAYERTYPE_SOUNDS)
4853 return std::static_pointer_cast<CLayerSounds>(pLayer)->m_Sound == i;
4854 return false;
4855 });
4856 });
4857
4858 if(!SoundUsed)
4859 Selected += 2; // Sound is unused
4860
4861 if(int Result = DoButton_Ex(pId: &m_Map.m_vpSounds[i], pText: m_Map.m_vpSounds[i]->m_aName, Checked: Selected, pRect: &Slot,
4862 Flags: BUTTON_CONTEXT, pToolTip: "Select sound.", Corners: IGraphics::CORNER_ALL))
4863 {
4864 m_SelectedSound = i;
4865
4866 if(Result == 2)
4867 {
4868 static SPopupMenuId s_PopupSoundId;
4869 Ui()->DoPopupMenu(pId: &s_PopupSoundId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 140, Height: 90, pContext: this, pfnFunc: PopupSound);
4870 }
4871 }
4872 }
4873
4874 // separator
4875 ToolBox.HSplitTop(Cut: 5.0f, pTop: &Slot, pBottom: &ToolBox);
4876 if(s_ScrollRegion.AddRect(Rect: Slot))
4877 {
4878 IGraphics::CLineItem LineItem(Slot.x, Slot.y + Slot.h / 2, Slot.x + Slot.w, Slot.y + Slot.h / 2);
4879 Graphics()->TextureClear();
4880 Graphics()->LinesBegin();
4881 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
4882 Graphics()->LinesEnd();
4883 }
4884
4885 // new sound
4886 static int s_AddSoundButton = 0;
4887 CUIRect AddSoundButton;
4888 ToolBox.HSplitTop(Cut: 5.0f + RowHeight + 1.0f, pTop: &AddSoundButton, pBottom: &ToolBox);
4889 if(s_ScrollRegion.AddRect(Rect: AddSoundButton))
4890 {
4891 AddSoundButton.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &AddSoundButton);
4892 AddSoundButton.HSplitTop(Cut: RowHeight, pTop: &AddSoundButton, pBottom: nullptr);
4893 if(DoButton_Editor(pId: &s_AddSoundButton, pText: "Add", Checked: 0, pRect: &AddSoundButton, Flags: 0, pToolTip: "Load a new sound to use in the map"))
4894 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_SOUND, pTitle: "Add Sound", pButtonText: "Add", pBasepath: "mapres", FilenameAsDefault: false, pfnFunc: AddSound, pUser: this);
4895 }
4896 s_ScrollRegion.End();
4897}
4898
4899static int EditorListdirCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser)
4900{
4901 CEditor *pEditor = (CEditor *)pUser;
4902 if((pInfo->m_pName[0] == '.' && (pInfo->m_pName[1] == 0 ||
4903 (pInfo->m_pName[1] == '.' && pInfo->m_pName[2] == 0 && (pEditor->m_FileDialogShowingRoot || (!pEditor->m_FileDialogMultipleStorages && (!str_comp(a: pEditor->m_pFileDialogPath, b: "maps") || !str_comp(a: pEditor->m_pFileDialogPath, b: "mapres"))))))) ||
4904 (!IsDir && ((pEditor->m_FileDialogFileType == CEditor::FILETYPE_MAP && !str_endswith(str: pInfo->m_pName, suffix: ".map")) ||
4905 (pEditor->m_FileDialogFileType == CEditor::FILETYPE_IMG && !str_endswith(str: pInfo->m_pName, suffix: ".png")) ||
4906 (pEditor->m_FileDialogFileType == CEditor::FILETYPE_SOUND && !str_endswith(str: pInfo->m_pName, suffix: ".opus")))))
4907 return 0;
4908
4909 CEditor::CFilelistItem Item;
4910 str_copy(dst&: Item.m_aFilename, src: pInfo->m_pName);
4911 if(IsDir)
4912 str_format(buffer: Item.m_aName, buffer_size: sizeof(Item.m_aName), format: "%s/", pInfo->m_pName);
4913 else
4914 {
4915 int LenEnding = pEditor->m_FileDialogFileType == CEditor::FILETYPE_SOUND ? 5 : 4;
4916 str_truncate(dst: Item.m_aName, dst_size: sizeof(Item.m_aName), src: pInfo->m_pName, truncation_len: str_length(str: pInfo->m_pName) - LenEnding);
4917 }
4918 Item.m_IsDir = IsDir != 0;
4919 Item.m_IsLink = false;
4920 Item.m_StorageType = StorageType;
4921 Item.m_TimeModified = pInfo->m_TimeModified;
4922 pEditor->m_vCompleteFileList.push_back(x: Item);
4923
4924 return 0;
4925}
4926
4927void CEditor::SortFilteredFileList()
4928{
4929 if(m_SortByFilename == 1)
4930 {
4931 std::sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareFilenameAscending);
4932 }
4933 else
4934 {
4935 std::sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareFilenameDescending);
4936 }
4937
4938 if(m_SortByTimeModified == 1)
4939 {
4940 std::stable_sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareTimeModifiedAscending);
4941 }
4942 else if(m_SortByTimeModified == -1)
4943 {
4944 std::stable_sort(first: m_vpFilteredFileList.begin(), last: m_vpFilteredFileList.end(), comp: CEditor::CompareTimeModifiedDescending);
4945 }
4946}
4947
4948void CEditor::RenderFileDialog()
4949{
4950 // GUI coordsys
4951 Ui()->MapScreen();
4952 CUIRect View = *Ui()->Screen();
4953 CUIRect Preview = {.x: 0.0f, .y: 0.0f, .w: 0.0f, .h: 0.0f};
4954 float Width = View.w, Height = View.h;
4955
4956 View.Draw(Color: ColorRGBA(0, 0, 0, 0.25f), Corners: 0, Rounding: 0);
4957 View.VMargin(Cut: 150.0f, pOtherRect: &View);
4958 View.HMargin(Cut: 50.0f, pOtherRect: &View);
4959 View.Draw(Color: ColorRGBA(0, 0, 0, 0.75f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f);
4960 View.Margin(Cut: 10.0f, pOtherRect: &View);
4961
4962 CUIRect Title, FileBox, FileBoxLabel, ButtonBar, PathBox;
4963 View.HSplitTop(Cut: 18.0f, pTop: &Title, pBottom: &View);
4964 View.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &View); // some spacing
4965 View.HSplitBottom(Cut: 14.0f, pTop: &View, pBottom: &ButtonBar);
4966 View.HSplitBottom(Cut: 10.0f, pTop: &View, pBottom: nullptr); // some spacing
4967 View.HSplitBottom(Cut: 14.0f, pTop: &View, pBottom: &PathBox);
4968 View.HSplitBottom(Cut: 5.0f, pTop: &View, pBottom: nullptr); // some spacing
4969 View.HSplitBottom(Cut: 14.0f, pTop: &View, pBottom: &FileBox);
4970 FileBox.VSplitLeft(Cut: 55.0f, pLeft: &FileBoxLabel, pRight: &FileBox);
4971 View.HSplitBottom(Cut: 10.0f, pTop: &View, pBottom: nullptr); // some spacing
4972 if(m_FileDialogFileType == CEditor::FILETYPE_IMG || m_FileDialogFileType == CEditor::FILETYPE_SOUND)
4973 View.VSplitMid(pLeft: &View, pRight: &Preview);
4974
4975 // title bar
4976 if(!m_FileDialogShowingRoot)
4977 {
4978 CUIRect ButtonTimeModified, ButtonFileName;
4979 Title.VSplitRight(Cut: 10.0f, pLeft: &Title, pRight: nullptr);
4980 Title.VSplitRight(Cut: 90.0f, pLeft: &Title, pRight: &ButtonTimeModified);
4981 Title.VSplitRight(Cut: 10.0f, pLeft: &Title, pRight: nullptr);
4982 Title.VSplitRight(Cut: 90.0f, pLeft: &Title, pRight: &ButtonFileName);
4983 Title.VSplitRight(Cut: 10.0f, pLeft: &Title, pRight: nullptr);
4984
4985 const char *aSortIndicator[3] = {"▼", "", "▲"};
4986
4987 static int s_ButtonTimeModified = 0;
4988 char aBufLabelButtonTimeModified[64];
4989 str_format(buffer: aBufLabelButtonTimeModified, buffer_size: sizeof(aBufLabelButtonTimeModified), format: "Time modified %s", aSortIndicator[m_SortByTimeModified + 1]);
4990 if(DoButton_Editor(pId: &s_ButtonTimeModified, pText: aBufLabelButtonTimeModified, Checked: 0, pRect: &ButtonTimeModified, Flags: 0, pToolTip: "Sort by time modified"))
4991 {
4992 if(m_SortByTimeModified == 1)
4993 {
4994 m_SortByTimeModified = -1;
4995 }
4996 else if(m_SortByTimeModified == -1)
4997 {
4998 m_SortByTimeModified = 0;
4999 }
5000 else
5001 {
5002 m_SortByTimeModified = 1;
5003 }
5004
5005 RefreshFilteredFileList();
5006 }
5007
5008 static int s_ButtonFileName = 0;
5009 char aBufLabelButtonFilename[64];
5010 str_format(buffer: aBufLabelButtonFilename, buffer_size: sizeof(aBufLabelButtonFilename), format: "Filename %s", aSortIndicator[m_SortByFilename + 1]);
5011 if(DoButton_Editor(pId: &s_ButtonFileName, pText: aBufLabelButtonFilename, Checked: 0, pRect: &ButtonFileName, Flags: 0, pToolTip: "Sort by file name"))
5012 {
5013 if(m_SortByFilename == 1)
5014 {
5015 m_SortByFilename = -1;
5016 m_SortByTimeModified = 0;
5017 }
5018 else
5019 {
5020 m_SortByFilename = 1;
5021 m_SortByTimeModified = 0;
5022 }
5023
5024 RefreshFilteredFileList();
5025 }
5026 }
5027
5028 Title.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f);
5029 Title.VMargin(Cut: 10.0f, pOtherRect: &Title);
5030 Ui()->DoLabel(pRect: &Title, pText: m_pFileDialogTitle, Size: 12.0f, Align: TEXTALIGN_ML);
5031
5032 // pathbox
5033 if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType >= IStorage::TYPE_SAVE)
5034 {
5035 char aPath[IO_MAX_PATH_LENGTH], aBuf[128 + IO_MAX_PATH_LENGTH];
5036 Storage()->GetCompletePath(Type: m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType, pDir: m_pFileDialogPath, pBuffer: aPath, BufferSize: sizeof(aPath));
5037 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Current path: %s", aPath);
5038 Ui()->DoLabel(pRect: &PathBox, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML);
5039 }
5040
5041 const auto &&UpdateFileNameInput = [this]() {
5042 if(m_FilesSelectedIndex >= 0 && !m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir)
5043 {
5044 char aNameWithoutExt[IO_MAX_PATH_LENGTH];
5045 fs_split_file_extension(filename: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, name: aNameWithoutExt, name_size: sizeof(aNameWithoutExt));
5046 m_FileDialogFileNameInput.Set(aNameWithoutExt);
5047 }
5048 else
5049 m_FileDialogFileNameInput.Clear();
5050 };
5051
5052 // filebox
5053 static CListBox s_ListBox;
5054 s_ListBox.SetActive(!Ui()->IsPopupOpen());
5055
5056 if(m_FileDialogStorageType == IStorage::TYPE_SAVE)
5057 {
5058 Ui()->DoLabel(pRect: &FileBoxLabel, pText: "Filename:", Size: 10.0f, Align: TEXTALIGN_ML);
5059 if(DoEditBox(pLineInput: &m_FileDialogFileNameInput, pRect: &FileBox, FontSize: 10.0f))
5060 {
5061 // remove '/' and '\'
5062 for(int i = 0; m_FileDialogFileNameInput.GetString()[i]; ++i)
5063 {
5064 if(m_FileDialogFileNameInput.GetString()[i] == '/' || m_FileDialogFileNameInput.GetString()[i] == '\\')
5065 {
5066 m_FileDialogFileNameInput.SetRange(pString: m_FileDialogFileNameInput.GetString() + i + 1, Begin: i, End: m_FileDialogFileNameInput.GetLength());
5067 --i;
5068 }
5069 }
5070 m_FilesSelectedIndex = -1;
5071 m_aFilesSelectedName[0] = '\0';
5072 // find first valid entry, if it exists
5073 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5074 {
5075 if(str_comp_nocase(a: m_vpFilteredFileList[i]->m_aName, b: m_FileDialogFileNameInput.GetString()) == 0)
5076 {
5077 m_FilesSelectedIndex = i;
5078 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[i]->m_aName);
5079 break;
5080 }
5081 }
5082 if(m_FilesSelectedIndex >= 0)
5083 s_ListBox.ScrollToSelected();
5084 }
5085
5086 if(m_FileDialogOpening)
5087 Ui()->SetActiveItem(&m_FileDialogFileNameInput);
5088 }
5089 else
5090 {
5091 // render search bar
5092 Ui()->DoLabel(pRect: &FileBoxLabel, pText: "Search:", Size: 10.0f, Align: TEXTALIGN_ML);
5093 if(m_FileDialogOpening || (Input()->KeyPress(Key: KEY_F) && Input()->ModifierIsPressed()))
5094 {
5095 Ui()->SetActiveItem(&m_FileDialogFilterInput);
5096 m_FileDialogFilterInput.SelectAll();
5097 }
5098 if(Ui()->DoClearableEditBox(pLineInput: &m_FileDialogFilterInput, pRect: &FileBox, FontSize: 10.0f))
5099 {
5100 RefreshFilteredFileList();
5101 if(m_vpFilteredFileList.empty())
5102 {
5103 m_FilesSelectedIndex = -1;
5104 }
5105 else if(m_FilesSelectedIndex == -1 || (!m_FileDialogFilterInput.IsEmpty() && !str_find_nocase(haystack: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName, needle: m_FileDialogFilterInput.GetString())))
5106 {
5107 // we need to refresh selection
5108 m_FilesSelectedIndex = -1;
5109 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5110 {
5111 if(str_find_nocase(haystack: m_vpFilteredFileList[i]->m_aName, needle: m_FileDialogFilterInput.GetString()))
5112 {
5113 m_FilesSelectedIndex = i;
5114 break;
5115 }
5116 }
5117 if(m_FilesSelectedIndex == -1)
5118 {
5119 // select first item
5120 m_FilesSelectedIndex = 0;
5121 }
5122 }
5123 if(m_FilesSelectedIndex >= 0)
5124 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5125 else
5126 m_aFilesSelectedName[0] = '\0';
5127 UpdateFileNameInput();
5128 s_ListBox.ScrollToSelected();
5129 m_FilePreviewState = PREVIEW_UNLOADED;
5130 }
5131 }
5132
5133 m_FileDialogOpening = false;
5134
5135 if(m_FilesSelectedIndex > -1)
5136 {
5137 if(m_FilePreviewState == PREVIEW_UNLOADED)
5138 {
5139 if(m_FileDialogFileType == CEditor::FILETYPE_IMG && str_endswith(str: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, suffix: ".png"))
5140 {
5141 char aBuffer[IO_MAX_PATH_LENGTH];
5142 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5143 if(Graphics()->LoadPng(Image&: m_FilePreviewImageInfo, pFilename: aBuffer, StorageType: m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType))
5144 {
5145 Graphics()->UnloadTexture(pIndex: &m_FilePreviewImage);
5146 m_FilePreviewImage = Graphics()->LoadTextureRawMove(Image&: m_FilePreviewImageInfo, Flags: 0, pTexName: aBuffer);
5147 m_FilePreviewImageInfo.m_pData = nullptr;
5148 m_FilePreviewState = PREVIEW_LOADED;
5149 }
5150 else
5151 {
5152 m_FilePreviewState = PREVIEW_ERROR;
5153 }
5154 }
5155 else if(m_FileDialogFileType == CEditor::FILETYPE_SOUND && str_endswith(str: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, suffix: ".opus"))
5156 {
5157 char aBuffer[IO_MAX_PATH_LENGTH];
5158 str_format(buffer: aBuffer, buffer_size: sizeof(aBuffer), format: "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5159 Sound()->UnloadSample(SampleId: m_FilePreviewSound);
5160 m_FilePreviewSound = Sound()->LoadOpus(pFilename: aBuffer, StorageType: m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType);
5161 m_FilePreviewState = m_FilePreviewSound == -1 ? PREVIEW_ERROR : PREVIEW_LOADED;
5162 }
5163 }
5164
5165 if(m_FileDialogFileType == CEditor::FILETYPE_IMG)
5166 {
5167 Preview.Margin(Cut: 10.0f, pOtherRect: &Preview);
5168 if(m_FilePreviewState == PREVIEW_LOADED)
5169 {
5170 int w = m_FilePreviewImageInfo.m_Width;
5171 int h = m_FilePreviewImageInfo.m_Height;
5172 if(m_FilePreviewImageInfo.m_Width > Preview.w)
5173 {
5174 h = m_FilePreviewImageInfo.m_Height * Preview.w / m_FilePreviewImageInfo.m_Width;
5175 w = Preview.w;
5176 }
5177 if(h > Preview.h)
5178 {
5179 w = w * Preview.h / h;
5180 h = Preview.h;
5181 }
5182
5183 Graphics()->TextureSet(Texture: m_FilePreviewImage);
5184 Graphics()->BlendNormal();
5185 Graphics()->QuadsBegin();
5186 IGraphics::CQuadItem QuadItem(Preview.x, Preview.y, w, h);
5187 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
5188 Graphics()->QuadsEnd();
5189 }
5190 else if(m_FilePreviewState == PREVIEW_ERROR)
5191 {
5192 SLabelProperties Props;
5193 Props.m_MaxWidth = Preview.w;
5194 Ui()->DoLabel(pRect: &Preview, pText: "Failed to load the image (check the local console for details).", Size: 12.0f, Align: TEXTALIGN_TL, LabelProps: Props);
5195 }
5196 }
5197 else if(m_FileDialogFileType == CEditor::FILETYPE_SOUND)
5198 {
5199 Preview.Margin(Cut: 10.0f, pOtherRect: &Preview);
5200 if(m_FilePreviewState == PREVIEW_LOADED)
5201 {
5202 Preview.HSplitTop(Cut: 20.0f, pTop: &Preview, pBottom: nullptr);
5203 Preview.VSplitLeft(Cut: Preview.h / 4.0f, pLeft: nullptr, pRight: &Preview);
5204
5205 static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0;
5206 DoAudioPreview(View: Preview, pPlayPauseButtonId: &s_PlayPauseButton, pStopButtonId: &s_StopButton, pSeekBarId: &s_SeekBar, SampleId: m_FilePreviewSound);
5207 }
5208 else if(m_FilePreviewState == PREVIEW_ERROR)
5209 {
5210 SLabelProperties Props;
5211 Props.m_MaxWidth = Preview.w;
5212 Ui()->DoLabel(pRect: &Preview, pText: "Failed to load the sound (check the local console for details). Make sure you enabled sounds in the settings.", Size: 12.0f, Align: TEXTALIGN_TL, LabelProps: Props);
5213 }
5214 }
5215 }
5216
5217 s_ListBox.DoStart(RowHeight: 15.0f, NumItems: m_vpFilteredFileList.size(), ItemsPerRow: 1, RowsPerScroll: 5, SelectedIndex: m_FilesSelectedIndex, pRect: &View, Background: false);
5218
5219 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5220 {
5221 const CListboxItem Item = s_ListBox.DoNextItem(pId: m_vpFilteredFileList[i], Selected: m_FilesSelectedIndex >= 0 && (size_t)m_FilesSelectedIndex == i);
5222 if(!Item.m_Visible)
5223 continue;
5224
5225 CUIRect Button, FileIcon, TimeModified;
5226 Item.m_Rect.VSplitLeft(Cut: Item.m_Rect.h, pLeft: &FileIcon, pRight: &Button);
5227 Button.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &Button);
5228 Button.VSplitRight(Cut: 100.0f, pLeft: &Button, pRight: &TimeModified);
5229 Button.VSplitRight(Cut: 5.0f, pLeft: &Button, pRight: nullptr);
5230
5231 const char *pIconType;
5232 if(!m_vpFilteredFileList[i]->m_IsDir)
5233 {
5234 switch(m_FileDialogFileType)
5235 {
5236 case FILETYPE_MAP:
5237 pIconType = FONT_ICON_MAP;
5238 break;
5239 case FILETYPE_IMG:
5240 pIconType = FONT_ICON_IMAGE;
5241 break;
5242 case FILETYPE_SOUND:
5243 pIconType = FONT_ICON_MUSIC;
5244 break;
5245 default:
5246 pIconType = FONT_ICON_FILE;
5247 }
5248 }
5249 else
5250 {
5251 if(m_vpFilteredFileList[i]->m_IsLink || str_comp(a: m_vpFilteredFileList[i]->m_aFilename, b: "..") == 0)
5252 pIconType = FONT_ICON_FOLDER_TREE;
5253 else
5254 pIconType = FONT_ICON_FOLDER;
5255 }
5256
5257 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
5258 TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING);
5259 Ui()->DoLabel(pRect: &FileIcon, pText: pIconType, Size: 12.0f, Align: TEXTALIGN_ML);
5260 TextRender()->SetRenderFlags(0);
5261 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
5262
5263 SLabelProperties Props;
5264 Props.m_MaxWidth = Button.w;
5265 Props.m_EllipsisAtEnd = true;
5266 Ui()->DoLabel(pRect: &Button, pText: m_vpFilteredFileList[i]->m_aName, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
5267
5268 if(!m_vpFilteredFileList[i]->m_IsLink && str_comp(a: m_vpFilteredFileList[i]->m_aFilename, b: "..") != 0)
5269 {
5270 char aBufTimeModified[64];
5271 str_timestamp_ex(time: m_vpFilteredFileList[i]->m_TimeModified, buffer: aBufTimeModified, buffer_size: sizeof(aBufTimeModified), format: "%d.%m.%Y %H:%M");
5272 Ui()->DoLabel(pRect: &TimeModified, pText: aBufTimeModified, Size: 10.0f, Align: TEXTALIGN_MR);
5273 }
5274 }
5275
5276 const int NewSelection = s_ListBox.DoEnd();
5277 if(NewSelection != m_FilesSelectedIndex)
5278 {
5279 m_FilesSelectedIndex = NewSelection;
5280 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5281 const bool WasChanged = m_FileDialogFileNameInput.WasChanged();
5282 UpdateFileNameInput();
5283 if(!WasChanged) // ensure that changed flag is not set if it wasn't previously set, as this would reset the selection after DoEditBox is called
5284 m_FileDialogFileNameInput.WasChanged(); // this clears the changed flag
5285 m_FilePreviewState = PREVIEW_UNLOADED;
5286 }
5287
5288 const float ButtonSpacing = ButtonBar.w > 600.0f ? 40.0f : 10.0f;
5289
5290 // the buttons
5291 static int s_OkButton = 0;
5292 static int s_CancelButton = 0;
5293 static int s_RefreshButton = 0;
5294 static int s_ShowDirectoryButton = 0;
5295 static int s_DeleteButton = 0;
5296 static int s_NewFolderButton = 0;
5297
5298 CUIRect Button;
5299 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5300 const bool IsDir = m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir;
5301 if(DoButton_Editor(pId: &s_OkButton, pText: IsDir ? "Open" : m_pFileDialogButtonText, Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || s_ListBox.WasItemActivated() || (s_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
5302 {
5303 if(IsDir) // folder
5304 {
5305 m_FileDialogFilterInput.Clear();
5306 Ui()->SetActiveItem(&m_FileDialogFilterInput);
5307 const bool ParentFolder = str_comp(a: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, b: "..") == 0;
5308 if(ParentFolder) // parent folder
5309 {
5310 str_copy(dst&: m_aFilesSelectedName, src: fs_filename(path: m_pFileDialogPath));
5311 str_append(dst&: m_aFilesSelectedName, src: "/");
5312 if(fs_parent_dir(path: m_pFileDialogPath))
5313 {
5314 if(str_comp(a: m_pFileDialogPath, b: m_aFileDialogCurrentFolder) == 0)
5315 {
5316 m_FileDialogShowingRoot = true;
5317 if(m_FileDialogStorageType == IStorage::TYPE_ALL)
5318 {
5319 m_aFilesSelectedName[0] = '\0'; // will select first list item
5320 }
5321 else
5322 {
5323 Storage()->GetCompletePath(Type: m_FileDialogStorageType, pDir: m_pFileDialogPath, pBuffer: m_aFilesSelectedName, BufferSize: sizeof(m_aFilesSelectedName));
5324 str_append(dst&: m_aFilesSelectedName, src: "/");
5325 }
5326 }
5327 else
5328 {
5329 m_pFileDialogPath = m_aFileDialogCurrentFolder; // leave the link
5330 str_copy(dst&: m_aFilesSelectedName, src: m_aFileDialogCurrentLink);
5331 str_append(dst&: m_aFilesSelectedName, src: "/");
5332 }
5333 }
5334 }
5335 else // sub folder
5336 {
5337 if(m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsLink)
5338 {
5339 m_pFileDialogPath = m_aFileDialogCurrentLink; // follow the link
5340 str_copy(dst&: m_aFileDialogCurrentLink, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5341 }
5342 else
5343 {
5344 char aTemp[IO_MAX_PATH_LENGTH];
5345 str_copy(dst&: aTemp, src: m_pFileDialogPath);
5346 str_format(buffer: m_pFileDialogPath, buffer_size: IO_MAX_PATH_LENGTH, format: "%s/%s", aTemp, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5347 }
5348 if(m_FileDialogShowingRoot)
5349 m_FileDialogStorageType = m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType;
5350 m_FileDialogShowingRoot = false;
5351 }
5352 FilelistPopulate(StorageType: m_FileDialogStorageType, KeepSelection: ParentFolder);
5353 UpdateFileNameInput();
5354 }
5355 else // file
5356 {
5357 const int StorageType = m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : m_FileDialogStorageType;
5358 str_format(buffer: m_aFileSaveName, buffer_size: sizeof(m_aFileSaveName), format: "%s/%s", m_pFileDialogPath, m_FileDialogFileNameInput.GetString());
5359 if(!str_endswith(str: m_aFileSaveName, suffix: FILETYPE_EXTENSIONS[m_FileDialogFileType]))
5360 str_append(dst&: m_aFileSaveName, src: FILETYPE_EXTENSIONS[m_FileDialogFileType]);
5361 if(!str_comp(a: m_pFileDialogButtonText, b: "Save") && Storage()->FileExists(pFilename: m_aFileSaveName, Type: StorageType))
5362 {
5363 if(m_pfnFileDialogFunc == &CallbackSaveMap)
5364 m_PopupEventType = POPEVENT_SAVE;
5365 else if(m_pfnFileDialogFunc == &CallbackSaveCopyMap)
5366 m_PopupEventType = POPEVENT_SAVE_COPY;
5367 else if(m_pfnFileDialogFunc == &CallbackSaveImage)
5368 m_PopupEventType = POPEVENT_SAVE_IMG;
5369 else
5370 m_PopupEventType = POPEVENT_SAVE_SOUND;
5371 m_PopupEventActivated = true;
5372 }
5373 else if(m_pfnFileDialogFunc)
5374 m_pfnFileDialogFunc(m_aFileSaveName, StorageType, m_pFileDialogUser);
5375 }
5376 }
5377
5378 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5379 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5380 if(DoButton_Editor(pId: &s_CancelButton, pText: "Cancel", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || (s_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
5381 {
5382 OnDialogClose();
5383 m_Dialog = DIALOG_NONE;
5384 }
5385
5386 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5387 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5388 if(DoButton_Editor(pId: &s_RefreshButton, pText: "Refresh", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || (s_ListBox.Active() && (Input()->KeyIsPressed(Key: KEY_F5) || (Input()->ModifierIsPressed() && Input()->KeyIsPressed(Key: KEY_R)))))
5389 FilelistPopulate(StorageType: m_FileDialogLastPopulatedStorageType, KeepSelection: true);
5390
5391 if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType != IStorage::TYPE_ALL)
5392 {
5393 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5394 ButtonBar.VSplitRight(Cut: 90.0f, pLeft: &ButtonBar, pRight: &Button);
5395 if(DoButton_Editor(pId: &s_ShowDirectoryButton, pText: "Show directory", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Open the current directory in the file browser"))
5396 {
5397 char aOpenPath[IO_MAX_PATH_LENGTH];
5398 Storage()->GetCompletePath(Type: m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, pDir: m_pFileDialogPath, pBuffer: aOpenPath, BufferSize: sizeof(aOpenPath));
5399 if(!Client()->ViewFile(pFilename: aOpenPath))
5400 {
5401 ShowFileDialogError(pFormat: "Failed to open the directory '%s'.", aOpenPath);
5402 }
5403 }
5404 }
5405
5406 ButtonBar.VSplitRight(Cut: ButtonSpacing, pLeft: &ButtonBar, pRight: nullptr);
5407 ButtonBar.VSplitRight(Cut: 50.0f, pLeft: &ButtonBar, pRight: &Button);
5408 static CUi::SConfirmPopupContext s_ConfirmDeletePopupContext;
5409 if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE && !m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsLink && str_comp(a: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, b: "..") != 0)
5410 {
5411 if(DoButton_Editor(pId: &s_DeleteButton, pText: "Delete", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr) || (s_ListBox.Active() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
5412 {
5413 s_ConfirmDeletePopupContext.Reset();
5414 s_ConfirmDeletePopupContext.YesNoButtons();
5415 str_format(buffer: s_ConfirmDeletePopupContext.m_aMessage, buffer_size: sizeof(s_ConfirmDeletePopupContext.m_aMessage), format: "Are you sure that you want to delete the %s '%s/%s'?", IsDir ? "folder" : "file", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5416 Ui()->ShowPopupConfirm(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext: &s_ConfirmDeletePopupContext);
5417 }
5418 if(s_ConfirmDeletePopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED)
5419 {
5420 char aDeleteFilePath[IO_MAX_PATH_LENGTH];
5421 str_format(buffer: aDeleteFilePath, buffer_size: sizeof(aDeleteFilePath), format: "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename);
5422 if(IsDir)
5423 {
5424 if(Storage()->RemoveFolder(pFilename: aDeleteFilePath, Type: IStorage::TYPE_SAVE))
5425 FilelistPopulate(StorageType: m_FileDialogLastPopulatedStorageType, KeepSelection: true);
5426 else
5427 ShowFileDialogError(pFormat: "Failed to delete folder '%s'. Make sure it's empty first.", aDeleteFilePath);
5428 }
5429 else
5430 {
5431 if(Storage()->RemoveFile(pFilename: aDeleteFilePath, Type: IStorage::TYPE_SAVE))
5432 FilelistPopulate(StorageType: m_FileDialogLastPopulatedStorageType, KeepSelection: true);
5433 else
5434 ShowFileDialogError(pFormat: "Failed to delete file '%s'.", aDeleteFilePath);
5435 }
5436 UpdateFileNameInput();
5437 }
5438 if(s_ConfirmDeletePopupContext.m_Result != CUi::SConfirmPopupContext::UNSET)
5439 s_ConfirmDeletePopupContext.Reset();
5440 }
5441 else
5442 s_ConfirmDeletePopupContext.Reset();
5443
5444 if(!m_FileDialogShowingRoot && m_FileDialogStorageType == IStorage::TYPE_SAVE)
5445 {
5446 ButtonBar.VSplitLeft(Cut: 70.0f, pLeft: &Button, pRight: &ButtonBar);
5447 if(DoButton_Editor(pId: &s_NewFolderButton, pText: "New folder", Checked: 0, pRect: &Button, Flags: 0, pToolTip: nullptr))
5448 {
5449 m_FileDialogNewFolderNameInput.Clear();
5450 static SPopupMenuId s_PopupNewFolderId;
5451 constexpr float PopupWidth = 400.0f;
5452 constexpr float PopupHeight = 110.0f;
5453 Ui()->DoPopupMenu(pId: &s_PopupNewFolderId, X: Width / 2.0f - PopupWidth / 2.0f, Y: Height / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight, pContext: this, pfnFunc: PopupNewFolder);
5454 Ui()->SetActiveItem(&m_FileDialogNewFolderNameInput);
5455 }
5456 }
5457}
5458
5459void CEditor::RefreshFilteredFileList()
5460{
5461 m_vpFilteredFileList.clear();
5462 for(const CFilelistItem &Item : m_vCompleteFileList)
5463 {
5464 if(m_FileDialogFilterInput.IsEmpty() || str_find_nocase(haystack: Item.m_aName, needle: m_FileDialogFilterInput.GetString()))
5465 {
5466 m_vpFilteredFileList.push_back(x: &Item);
5467 }
5468 }
5469 if(!m_FileDialogShowingRoot)
5470 SortFilteredFileList();
5471 if(!m_vpFilteredFileList.empty())
5472 {
5473 if(m_aFilesSelectedName[0])
5474 {
5475 for(size_t i = 0; i < m_vpFilteredFileList.size(); i++)
5476 {
5477 if(m_aFilesSelectedName[0] && str_comp(a: m_vpFilteredFileList[i]->m_aName, b: m_aFilesSelectedName) == 0)
5478 {
5479 m_FilesSelectedIndex = i;
5480 break;
5481 }
5482 }
5483 }
5484 m_FilesSelectedIndex = clamp<int>(val: m_FilesSelectedIndex, lo: 0, hi: m_vpFilteredFileList.size() - 1);
5485 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5486 }
5487 else
5488 {
5489 m_FilesSelectedIndex = -1;
5490 m_aFilesSelectedName[0] = '\0';
5491 }
5492}
5493
5494void CEditor::FilelistPopulate(int StorageType, bool KeepSelection)
5495{
5496 m_FileDialogLastPopulatedStorageType = StorageType;
5497 m_vCompleteFileList.clear();
5498 if(m_FileDialogShowingRoot)
5499 {
5500 {
5501 CFilelistItem Item;
5502 str_copy(dst&: Item.m_aFilename, src: m_pFileDialogPath);
5503 str_copy(dst&: Item.m_aName, src: "All combined");
5504 Item.m_IsDir = true;
5505 Item.m_IsLink = true;
5506 Item.m_StorageType = IStorage::TYPE_ALL;
5507 Item.m_TimeModified = 0;
5508 m_vCompleteFileList.push_back(x: Item);
5509 }
5510
5511 for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType)
5512 {
5513 if(Storage()->FolderExists(pFilename: m_pFileDialogPath, Type: CheckStorageType))
5514 {
5515 CFilelistItem Item;
5516 str_copy(dst&: Item.m_aFilename, src: m_pFileDialogPath);
5517 Storage()->GetCompletePath(Type: CheckStorageType, pDir: m_pFileDialogPath, pBuffer: Item.m_aName, BufferSize: sizeof(Item.m_aName));
5518 str_append(dst: Item.m_aName, src: "/", dst_size: sizeof(Item.m_aName));
5519 Item.m_IsDir = true;
5520 Item.m_IsLink = true;
5521 Item.m_StorageType = CheckStorageType;
5522 Item.m_TimeModified = 0;
5523 m_vCompleteFileList.push_back(x: Item);
5524 }
5525 }
5526 }
5527 else
5528 {
5529 // Add links for downloadedmaps and themes
5530 if(!str_comp(a: m_pFileDialogPath, b: "maps"))
5531 {
5532 if(str_comp(a: m_pFileDialogButtonText, b: "Save") != 0 && Storage()->FolderExists(pFilename: "downloadedmaps", Type: StorageType))
5533 {
5534 CFilelistItem Item;
5535 str_copy(dst&: Item.m_aFilename, src: "downloadedmaps");
5536 str_copy(dst&: Item.m_aName, src: "downloadedmaps/");
5537 Item.m_IsDir = true;
5538 Item.m_IsLink = true;
5539 Item.m_StorageType = StorageType;
5540 Item.m_TimeModified = 0;
5541 m_vCompleteFileList.push_back(x: Item);
5542 }
5543
5544 if(Storage()->FolderExists(pFilename: "themes", Type: StorageType))
5545 {
5546 CFilelistItem Item;
5547 str_copy(dst&: Item.m_aFilename, src: "themes");
5548 str_copy(dst&: Item.m_aName, src: "themes/");
5549 Item.m_IsDir = true;
5550 Item.m_IsLink = true;
5551 Item.m_StorageType = StorageType;
5552 Item.m_TimeModified = 0;
5553 m_vCompleteFileList.push_back(x: Item);
5554 }
5555 }
5556 Storage()->ListDirectoryInfo(Type: StorageType, pPath: m_pFileDialogPath, pfnCallback: EditorListdirCallback, pUser: this);
5557 }
5558 RefreshFilteredFileList();
5559 if(!KeepSelection)
5560 {
5561 m_FilesSelectedIndex = m_vpFilteredFileList.empty() ? -1 : 0;
5562 if(m_FilesSelectedIndex >= 0)
5563 str_copy(dst&: m_aFilesSelectedName, src: m_vpFilteredFileList[m_FilesSelectedIndex]->m_aName);
5564 else
5565 m_aFilesSelectedName[0] = '\0';
5566 }
5567 m_FilePreviewState = PREVIEW_UNLOADED;
5568}
5569
5570void CEditor::InvokeFileDialog(int StorageType, int FileType, const char *pTitle, const char *pButtonText,
5571 const char *pBasePath, bool FilenameAsDefault,
5572 bool (*pfnFunc)(const char *pFileName, int StorageType, void *pUser), void *pUser)
5573{
5574 m_FileDialogStorageType = StorageType;
5575 if(m_FileDialogStorageType == IStorage::TYPE_ALL)
5576 {
5577 int NumStoragesWithFolder = 0;
5578 for(int CheckStorageType = IStorage::TYPE_SAVE; CheckStorageType < Storage()->NumPaths(); ++CheckStorageType)
5579 {
5580 if(Storage()->FolderExists(pFilename: pBasePath, Type: CheckStorageType))
5581 {
5582 NumStoragesWithFolder++;
5583 }
5584 }
5585 m_FileDialogMultipleStorages = NumStoragesWithFolder > 1;
5586 }
5587 else
5588 {
5589 m_FileDialogMultipleStorages = false;
5590 }
5591
5592 Ui()->ClosePopupMenus();
5593 m_pFileDialogTitle = pTitle;
5594 m_pFileDialogButtonText = pButtonText;
5595 m_pfnFileDialogFunc = pfnFunc;
5596 m_pFileDialogUser = pUser;
5597 m_FileDialogFileNameInput.Clear();
5598 m_FileDialogFilterInput.Clear();
5599 m_aFileDialogCurrentFolder[0] = 0;
5600 m_aFileDialogCurrentLink[0] = 0;
5601 m_pFileDialogPath = m_aFileDialogCurrentFolder;
5602 m_FileDialogFileType = FileType;
5603 m_FilePreviewState = PREVIEW_UNLOADED;
5604 m_FileDialogOpening = true;
5605 m_FileDialogShowingRoot = false;
5606
5607 if(FilenameAsDefault)
5608 {
5609 char aDefaultName[IO_MAX_PATH_LENGTH];
5610 fs_split_file_extension(filename: fs_filename(path: m_aFileName), name: aDefaultName, name_size: sizeof(aDefaultName));
5611 m_FileDialogFileNameInput.Set(aDefaultName);
5612 }
5613 if(pBasePath)
5614 str_copy(dst&: m_aFileDialogCurrentFolder, src: pBasePath);
5615
5616 FilelistPopulate(StorageType: m_FileDialogStorageType);
5617
5618 m_FileDialogOpening = true;
5619 m_Dialog = DIALOG_FILE;
5620}
5621
5622void CEditor::ShowFileDialogError(const char *pFormat, ...)
5623{
5624 char aMessage[1024];
5625 va_list VarArgs;
5626 va_start(VarArgs, pFormat);
5627 str_format_v(buffer: aMessage, buffer_size: sizeof(aMessage), format: pFormat, args: VarArgs);
5628 va_end(VarArgs);
5629
5630 auto ContextIterator = m_PopupMessageContexts.find(x: aMessage);
5631 CUi::SMessagePopupContext *pContext;
5632 if(ContextIterator != m_PopupMessageContexts.end())
5633 {
5634 pContext = ContextIterator->second;
5635 Ui()->ClosePopupMenu(pId: pContext);
5636 }
5637 else
5638 {
5639 pContext = new CUi::SMessagePopupContext();
5640 pContext->ErrorColor();
5641 str_copy(dst&: pContext->m_aMessage, src: aMessage);
5642 m_PopupMessageContexts[pContext->m_aMessage] = pContext;
5643 }
5644 Ui()->ShowPopupMessage(X: Ui()->MouseX(), Y: Ui()->MouseY(), pContext);
5645}
5646
5647void CEditor::RenderModebar(CUIRect View)
5648{
5649 CUIRect Mentions, IngameMoved, ModeButtons, ModeButton;
5650 View.HSplitTop(Cut: 12.0f, pTop: &Mentions, pBottom: &View);
5651 View.HSplitTop(Cut: 12.0f, pTop: &IngameMoved, pBottom: &View);
5652 View.HSplitTop(Cut: 8.0f, pTop: nullptr, pBottom: &ModeButtons);
5653 const float Width = m_ToolBoxWidth - 5.0f;
5654 ModeButtons.VSplitLeft(Cut: Width, pLeft: &ModeButtons, pRight: nullptr);
5655 const float ButtonWidth = Width / 3;
5656
5657 // mentions
5658 if(m_Mentions)
5659 {
5660 char aBuf[64];
5661 if(m_Mentions == 1)
5662 str_copy(dst&: aBuf, src: Localize(pStr: "1 new mention"));
5663 else if(m_Mentions <= 9)
5664 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: Localize(pStr: "%d new mentions"), m_Mentions);
5665 else
5666 str_copy(dst&: aBuf, src: Localize(pStr: "9+ new mentions"));
5667
5668 TextRender()->TextColor(rgb: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
5669 Ui()->DoLabel(pRect: &Mentions, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MC);
5670 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
5671 }
5672
5673 // ingame moved warning
5674 if(m_IngameMoved)
5675 {
5676 TextRender()->TextColor(rgb: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f));
5677 Ui()->DoLabel(pRect: &IngameMoved, pText: Localize(pStr: "Moved ingame"), Size: 10.0f, Align: TEXTALIGN_MC);
5678 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
5679 }
5680
5681 // mode buttons
5682 {
5683 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
5684 static int s_LayersButton = 0;
5685 if(DoButton_FontIcon(pId: &s_LayersButton, pText: FONT_ICON_LAYER_GROUP, Checked: m_Mode == MODE_LAYERS, pRect: &ModeButton, Flags: 0, pToolTip: "Go to layers management.", Corners: IGraphics::CORNER_L))
5686 {
5687 m_Mode = MODE_LAYERS;
5688 }
5689
5690 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
5691 static int s_ImagesButton = 0;
5692 if(DoButton_FontIcon(pId: &s_ImagesButton, pText: FONT_ICON_IMAGE, Checked: m_Mode == MODE_IMAGES, pRect: &ModeButton, Flags: 0, pToolTip: "Go to images management.", Corners: IGraphics::CORNER_NONE))
5693 {
5694 m_Mode = MODE_IMAGES;
5695 }
5696
5697 ModeButtons.VSplitLeft(Cut: ButtonWidth, pLeft: &ModeButton, pRight: &ModeButtons);
5698 static int s_SoundsButton = 0;
5699 if(DoButton_FontIcon(pId: &s_SoundsButton, pText: FONT_ICON_MUSIC, Checked: m_Mode == MODE_SOUNDS, pRect: &ModeButton, Flags: 0, pToolTip: "Go to sounds management.", Corners: IGraphics::CORNER_R))
5700 {
5701 m_Mode = MODE_SOUNDS;
5702 }
5703
5704 if(Input()->KeyPress(Key: KEY_LEFT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
5705 {
5706 m_Mode = (m_Mode + NUM_MODES - 1) % NUM_MODES;
5707 }
5708 else if(Input()->KeyPress(Key: KEY_RIGHT) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
5709 {
5710 m_Mode = (m_Mode + 1) % NUM_MODES;
5711 }
5712 }
5713}
5714
5715void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect)
5716{
5717 const bool ButtonsDisabled = m_ShowPicker;
5718
5719 CUIRect Button;
5720 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
5721 static int s_EnvelopeButton = 0;
5722 if(DoButton_Editor(pId: &s_EnvelopeButton, pText: "Envelopes", Checked: ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES, pRect: &Button, Flags: 0, pToolTip: "Toggles the envelope editor.") == 1)
5723 {
5724 m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES ? EXTRAEDITOR_NONE : EXTRAEDITOR_ENVELOPES;
5725 }
5726
5727 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
5728 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
5729 static int s_SettingsButton = 0;
5730 if(DoButton_Editor(pId: &s_SettingsButton, pText: "Server settings", Checked: ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS, pRect: &Button, Flags: 0, pToolTip: "Toggles the server settings editor.") == 1)
5731 {
5732 m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS ? EXTRAEDITOR_NONE : EXTRAEDITOR_SERVER_SETTINGS;
5733 }
5734
5735 View.VSplitRight(Cut: 10.0f, pLeft: &View, pRight: nullptr);
5736 View.VSplitRight(Cut: 100.0f, pLeft: &View, pRight: &Button);
5737 static int s_HistoryButton = 0;
5738 if(DoButton_Editor(pId: &s_HistoryButton, pText: "History", Checked: ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_HISTORY, pRect: &Button, Flags: 0, pToolTip: "Toggles the editor history view.") == 1)
5739 {
5740 m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_HISTORY ? EXTRAEDITOR_NONE : EXTRAEDITOR_HISTORY;
5741 }
5742
5743 View.VSplitRight(Cut: 10.0f, pLeft: pTooltipRect, pRight: nullptr);
5744}
5745
5746void CEditor::RenderTooltip(CUIRect TooltipRect)
5747{
5748 if(str_comp(a: m_aTooltip, b: "") == 0)
5749 return;
5750
5751 char aBuf[256];
5752 if(ms_pUiGotContext && ms_pUiGotContext == Ui()->HotItem())
5753 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%s Right click for context menu.", m_aTooltip);
5754 else
5755 str_copy(dst&: aBuf, src: m_aTooltip);
5756
5757 SLabelProperties Props;
5758 Props.m_MaxWidth = TooltipRect.w;
5759 Props.m_EllipsisAtEnd = true;
5760 Ui()->DoLabel(pRect: &TooltipRect, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
5761}
5762
5763bool CEditor::IsEnvelopeUsed(int EnvelopeIndex) const
5764{
5765 for(const auto &pGroup : m_Map.m_vpGroups)
5766 {
5767 for(const auto &pLayer : pGroup->m_vpLayers)
5768 {
5769 if(pLayer->m_Type == LAYERTYPE_QUADS)
5770 {
5771 std::shared_ptr<CLayerQuads> pLayerQuads = std::static_pointer_cast<CLayerQuads>(r: pLayer);
5772 for(const auto &Quad : pLayerQuads->m_vQuads)
5773 {
5774 if(Quad.m_PosEnv == EnvelopeIndex || Quad.m_ColorEnv == EnvelopeIndex)
5775 {
5776 return true;
5777 }
5778 }
5779 }
5780 else if(pLayer->m_Type == LAYERTYPE_SOUNDS)
5781 {
5782 std::shared_ptr<CLayerSounds> pLayerSounds = std::static_pointer_cast<CLayerSounds>(r: pLayer);
5783 for(const auto &Source : pLayerSounds->m_vSources)
5784 {
5785 if(Source.m_PosEnv == EnvelopeIndex || Source.m_SoundEnv == EnvelopeIndex)
5786 {
5787 return true;
5788 }
5789 }
5790 }
5791 else if(pLayer->m_Type == LAYERTYPE_TILES)
5792 {
5793 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
5794 if(pLayerTiles->m_ColorEnv == EnvelopeIndex)
5795 return true;
5796 }
5797 }
5798 }
5799 return false;
5800}
5801
5802void CEditor::RemoveUnusedEnvelopes()
5803{
5804 m_EnvelopeEditorHistory.BeginBulk();
5805 int DeletedCount = 0;
5806 for(size_t Envelope = 0; Envelope < m_Map.m_vpEnvelopes.size();)
5807 {
5808 if(IsEnvelopeUsed(EnvelopeIndex: Envelope))
5809 {
5810 ++Envelope;
5811 }
5812 else
5813 {
5814 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEveloppeDelete>(args: this, args&: Envelope));
5815 m_Map.DeleteEnvelope(Index: Envelope);
5816 DeletedCount++;
5817 }
5818 }
5819 char aDisplay[256];
5820 str_format(buffer: aDisplay, buffer_size: sizeof(aDisplay), format: "Tool 'Remove unused envelopes': delete %d envelopes", DeletedCount);
5821 m_EnvelopeEditorHistory.EndBulk(pDisplay: aDisplay);
5822}
5823
5824void CEditor::ZoomAdaptOffsetX(float ZoomFactor, const CUIRect &View)
5825{
5826 float PosX = g_Config.m_EdZoomTarget ? (Ui()->MouseX() - View.x) / View.w : 0.5f;
5827 m_OffsetEnvelopeX = PosX - (PosX - m_OffsetEnvelopeX) * ZoomFactor;
5828}
5829
5830void CEditor::UpdateZoomEnvelopeX(const CUIRect &View)
5831{
5832 float OldZoom = m_ZoomEnvelopeX.GetValue();
5833 if(m_ZoomEnvelopeX.UpdateValue())
5834 ZoomAdaptOffsetX(ZoomFactor: OldZoom / m_ZoomEnvelopeX.GetValue(), View);
5835}
5836
5837void CEditor::ZoomAdaptOffsetY(float ZoomFactor, const CUIRect &View)
5838{
5839 float PosY = g_Config.m_EdZoomTarget ? 1.0f - (Ui()->MouseY() - View.y) / View.h : 0.5f;
5840 m_OffsetEnvelopeY = PosY - (PosY - m_OffsetEnvelopeY) * ZoomFactor;
5841}
5842
5843void CEditor::UpdateZoomEnvelopeY(const CUIRect &View)
5844{
5845 float OldZoom = m_ZoomEnvelopeY.GetValue();
5846 if(m_ZoomEnvelopeY.UpdateValue())
5847 ZoomAdaptOffsetY(ZoomFactor: OldZoom / m_ZoomEnvelopeY.GetValue(), View);
5848}
5849
5850void CEditor::ResetZoomEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope, int ActiveChannels)
5851{
5852 auto [Bottom, Top] = pEnvelope->GetValueRange(ChannelMask: ActiveChannels);
5853 float EndTime = pEnvelope->EndTime();
5854 float ValueRange = absolute(a: Top - Bottom);
5855
5856 if(ValueRange < m_ZoomEnvelopeY.GetMinValue())
5857 {
5858 // Set view to some sane default if range is too small
5859 m_OffsetEnvelopeY = 0.5f - ValueRange / m_ZoomEnvelopeY.GetMinValue() / 2.0f - Bottom / m_ZoomEnvelopeY.GetMinValue();
5860 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMinValue());
5861 }
5862 else if(ValueRange > m_ZoomEnvelopeY.GetMaxValue())
5863 {
5864 m_OffsetEnvelopeY = -Bottom / m_ZoomEnvelopeY.GetMaxValue();
5865 m_ZoomEnvelopeY.SetValueInstant(m_ZoomEnvelopeY.GetMaxValue());
5866 }
5867 else
5868 {
5869 // calculate biggest possible spacing
5870 float SpacingFactor = minimum(a: 1.25f, b: m_ZoomEnvelopeY.GetMaxValue() / ValueRange);
5871 m_ZoomEnvelopeY.SetValueInstant(SpacingFactor * ValueRange);
5872 float Space = 1.0f / SpacingFactor;
5873 float Spacing = (1.0f - Space) / 2.0f;
5874
5875 if(Top >= 0 && Bottom >= 0)
5876 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
5877 else if(Top <= 0 && Bottom <= 0)
5878 m_OffsetEnvelopeY = Spacing - Bottom / m_ZoomEnvelopeY.GetValue();
5879 else
5880 m_OffsetEnvelopeY = Spacing + Space * absolute(a: Bottom) / ValueRange;
5881 }
5882
5883 if(EndTime < m_ZoomEnvelopeX.GetMinValue())
5884 {
5885 m_OffsetEnvelopeX = 0.5f - EndTime / m_ZoomEnvelopeX.GetMinValue();
5886 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMinValue());
5887 }
5888 else if(EndTime > m_ZoomEnvelopeX.GetMaxValue())
5889 {
5890 m_OffsetEnvelopeX = 0.0f;
5891 m_ZoomEnvelopeX.SetValueInstant(m_ZoomEnvelopeX.GetMaxValue());
5892 }
5893 else
5894 {
5895 float SpacingFactor = minimum(a: 1.25f, b: m_ZoomEnvelopeX.GetMaxValue() / EndTime);
5896 m_ZoomEnvelopeX.SetValueInstant(SpacingFactor * EndTime);
5897 float Space = 1.0f / SpacingFactor;
5898 float Spacing = (1.0f - Space) / 2.0f;
5899
5900 m_OffsetEnvelopeX = Spacing;
5901 }
5902}
5903
5904float fxt2f(int t)
5905{
5906 return t / 1000.0f;
5907}
5908
5909int f2fxt(float t)
5910{
5911 return static_cast<int>(t * 1000.0f);
5912}
5913
5914float CEditor::ScreenToEnvelopeX(const CUIRect &View, float x) const
5915{
5916 return (x - View.x - View.w * m_OffsetEnvelopeX) / View.w * m_ZoomEnvelopeX.GetValue();
5917}
5918
5919float CEditor::EnvelopeToScreenX(const CUIRect &View, float x) const
5920{
5921 return View.x + View.w * m_OffsetEnvelopeX + x / m_ZoomEnvelopeX.GetValue() * View.w;
5922}
5923
5924float CEditor::ScreenToEnvelopeY(const CUIRect &View, float y) const
5925{
5926 return (View.h - y + View.y) / View.h * m_ZoomEnvelopeY.GetValue() - m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue();
5927}
5928
5929float CEditor::EnvelopeToScreenY(const CUIRect &View, float y) const
5930{
5931 return View.y + View.h - y / m_ZoomEnvelopeY.GetValue() * View.h - m_OffsetEnvelopeY * View.h;
5932}
5933
5934float CEditor::ScreenToEnvelopeDX(const CUIRect &View, float dx)
5935{
5936 return dx / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w * m_ZoomEnvelopeX.GetValue();
5937}
5938
5939float CEditor::ScreenToEnvelopeDY(const CUIRect &View, float dy)
5940{
5941 return dy / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h * m_ZoomEnvelopeY.GetValue();
5942}
5943
5944void CEditor::RemoveTimeOffsetEnvelope(const std::shared_ptr<CEnvelope> &pEnvelope)
5945{
5946 int TimeOffset = pEnvelope->m_vPoints[0].m_Time;
5947 for(auto &Point : pEnvelope->m_vPoints)
5948 Point.m_Time -= TimeOffset;
5949
5950 m_OffsetEnvelopeX += fxt2f(t: TimeOffset) / m_ZoomEnvelopeX.GetValue();
5951};
5952
5953static float ClampDelta(float Val, float Delta, float Min, float Max)
5954{
5955 if(Val + Delta <= Min)
5956 return Min - Val;
5957 if(Val + Delta >= Max)
5958 return Max - Val;
5959 return Delta;
5960}
5961
5962class CTimeStep
5963{
5964public:
5965 template<class T>
5966 CTimeStep(T t)
5967 {
5968 if constexpr(std::is_same_v<T, std::chrono::milliseconds>)
5969 m_Unit = ETimeUnit::MILLISECONDS;
5970 else if constexpr(std::is_same_v<T, std::chrono::seconds>)
5971 m_Unit = ETimeUnit::SECONDS;
5972 else
5973 m_Unit = ETimeUnit::MINUTES;
5974
5975 m_Value = t;
5976 }
5977
5978 CTimeStep operator*(int k) const
5979 {
5980 return CTimeStep(m_Value * k, m_Unit);
5981 }
5982
5983 CTimeStep operator-(const CTimeStep &Other)
5984 {
5985 return CTimeStep(m_Value - Other.m_Value, m_Unit);
5986 }
5987
5988 void Format(char *pBuffer, size_t BufferSize)
5989 {
5990 int Milliseconds = m_Value.count() % 1000;
5991 int Seconds = std::chrono::duration_cast<std::chrono::seconds>(d: m_Value).count() % 60;
5992 int Minutes = std::chrono::duration_cast<std::chrono::minutes>(d: m_Value).count();
5993
5994 switch(m_Unit)
5995 {
5996 case ETimeUnit::MILLISECONDS:
5997 if(Minutes != 0)
5998 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02d.%03dmin", Minutes, Seconds, Milliseconds);
5999 else if(Seconds != 0)
6000 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d.%03ds", Seconds, Milliseconds);
6001 else
6002 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dms", Milliseconds);
6003 break;
6004 case ETimeUnit::SECONDS:
6005 if(Minutes != 0)
6006 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%d:%02dmin", Minutes, Seconds);
6007 else
6008 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%ds", Seconds);
6009 break;
6010 case ETimeUnit::MINUTES:
6011 str_format(buffer: pBuffer, buffer_size: BufferSize, format: "%dmin", Minutes);
6012 break;
6013 }
6014 }
6015
6016 float AsSeconds() const
6017 {
6018 return std::chrono::duration_cast<std::chrono::duration<float>>(d: m_Value).count();
6019 }
6020
6021private:
6022 enum class ETimeUnit
6023 {
6024 MILLISECONDS,
6025 SECONDS,
6026 MINUTES
6027 } m_Unit;
6028 std::chrono::milliseconds m_Value;
6029
6030 CTimeStep(std::chrono::milliseconds Value, ETimeUnit Unit)
6031 {
6032 m_Value = Value;
6033 m_Unit = Unit;
6034 }
6035};
6036
6037void CEditor::SetHotEnvelopePoint(const CUIRect &View, const std::shared_ptr<CEnvelope> &pEnvelope, int ActiveChannels)
6038{
6039 if(!Ui()->MouseInside(pRect: &View))
6040 return;
6041
6042 float mx = Ui()->MouseX();
6043 float my = Ui()->MouseY();
6044
6045 float MinDist = 200.0f;
6046 int *pMinPoint = nullptr;
6047
6048 auto UpdateMinimum = [&](float px, float py, int *pId) {
6049 float dx = px - mx;
6050 float dy = py - my;
6051
6052 float CurrDist = dx * dx + dy * dy;
6053 if(CurrDist < MinDist)
6054 {
6055 MinDist = CurrDist;
6056 pMinPoint = pId;
6057 }
6058 };
6059
6060 for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++)
6061 {
6062 for(int c = pEnvelope->GetChannels() - 1; c >= 0; c--)
6063 {
6064 if(!(ActiveChannels & (1 << c)))
6065 continue;
6066
6067 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
6068 {
6069 float px = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]));
6070 float py = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
6071 UpdateMinimum(px, py, &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]);
6072 }
6073
6074 if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
6075 {
6076 float px = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]));
6077 float py = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
6078 UpdateMinimum(px, py, &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]);
6079 }
6080
6081 float px = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6082 float py = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
6083 UpdateMinimum(px, py, &pEnvelope->m_vPoints[i].m_aValues[c]);
6084 }
6085 }
6086
6087 if(pMinPoint != nullptr)
6088 Ui()->SetHotItem(pMinPoint);
6089}
6090
6091void CEditor::RenderEnvelopeEditor(CUIRect View)
6092{
6093 if(m_SelectedEnvelope < 0)
6094 m_SelectedEnvelope = 0;
6095 if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
6096 m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1;
6097
6098 std::shared_ptr<CEnvelope> pEnvelope = nullptr;
6099 if(m_SelectedEnvelope >= 0 && m_SelectedEnvelope < (int)m_Map.m_vpEnvelopes.size())
6100 pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
6101
6102 static EEnvelopeEditorOp s_Operation = EEnvelopeEditorOp::OP_NONE;
6103 static std::vector<float> s_vAccurateDragValuesX = {};
6104 static std::vector<float> s_vAccurateDragValuesY = {};
6105 static float s_MouseXStart = 0.0f;
6106 static float s_MouseYStart = 0.0f;
6107
6108 static CLineInput s_NameInput;
6109
6110 CUIRect ToolBar, CurveBar, ColorBar, DragBar;
6111 View.HSplitTop(Cut: 30.0f, pTop: &DragBar, pBottom: nullptr);
6112 DragBar.y -= 2.0f;
6113 DragBar.w += 2.0f;
6114 DragBar.h += 4.0f;
6115 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::SIDE_TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_ENVELOPES]);
6116 View.HSplitTop(Cut: 15.0f, pTop: &ToolBar, pBottom: &View);
6117 View.HSplitTop(Cut: 15.0f, pTop: &CurveBar, pBottom: &View);
6118 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
6119 CurveBar.Margin(Cut: 2.0f, pOtherRect: &CurveBar);
6120
6121 bool CurrentEnvelopeSwitched = false;
6122
6123 // do the toolbar
6124 static int s_ActiveChannels = 0xf;
6125 {
6126 CUIRect Button;
6127 std::shared_ptr<CEnvelope> pNewEnv = nullptr;
6128
6129 // redo button
6130 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6131 static int s_RedoButton = 0;
6132 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FONT_ICON_REDO, Checked: m_EnvelopeEditorHistory.CanRedo() ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Y] Redo last action", Corners: IGraphics::CORNER_R, FontSize: 11.0f) == 1)
6133 {
6134 m_EnvelopeEditorHistory.Redo();
6135 }
6136
6137 // undo button
6138 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6139 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
6140 static int s_UndoButton = 0;
6141 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FONT_ICON_UNDO, Checked: m_EnvelopeEditorHistory.CanUndo() ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Z] Undo last action", Corners: IGraphics::CORNER_L, FontSize: 11.0f) == 1)
6142 {
6143 m_EnvelopeEditorHistory.Undo();
6144 }
6145
6146 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
6147 static int s_NewSoundButton = 0;
6148 if(DoButton_Editor(pId: &s_NewSoundButton, pText: "Sound+", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Creates a new sound envelope"))
6149 {
6150 m_Map.OnModify();
6151 pNewEnv = m_Map.NewEnvelope(Type: CEnvelope::EType::SOUND);
6152 }
6153
6154 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6155 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
6156 static int s_New4dButton = 0;
6157 if(DoButton_Editor(pId: &s_New4dButton, pText: "Color+", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Creates a new color envelope"))
6158 {
6159 m_Map.OnModify();
6160 pNewEnv = m_Map.NewEnvelope(Type: CEnvelope::EType::COLOR);
6161 }
6162
6163 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6164 ToolBar.VSplitRight(Cut: 50.0f, pLeft: &ToolBar, pRight: &Button);
6165 static int s_New2dButton = 0;
6166 if(DoButton_Editor(pId: &s_New2dButton, pText: "Pos.+", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Creates a new position envelope"))
6167 {
6168 m_Map.OnModify();
6169 pNewEnv = m_Map.NewEnvelope(Type: CEnvelope::EType::POSITION);
6170 }
6171
6172 if(m_SelectedEnvelope >= 0)
6173 {
6174 // Delete button
6175 ToolBar.VSplitRight(Cut: 10.0f, pLeft: &ToolBar, pRight: nullptr);
6176 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6177 static int s_DeleteButton = 0;
6178 if(DoButton_Editor(pId: &s_DeleteButton, pText: "✗", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Delete this envelope"))
6179 {
6180 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEveloppeDelete>(args: this, args&: m_SelectedEnvelope));
6181 m_Map.DeleteEnvelope(Index: m_SelectedEnvelope);
6182 if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
6183 m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1;
6184 pEnvelope = m_SelectedEnvelope >= 0 ? m_Map.m_vpEnvelopes[m_SelectedEnvelope] : nullptr;
6185 m_Map.OnModify();
6186 }
6187
6188 // Move right button
6189 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6190 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6191 static int s_MoveRightButton = 0;
6192 if(DoButton_Ex(pId: &s_MoveRightButton, pText: "→", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Move this envelope to the right", Corners: IGraphics::CORNER_R))
6193 {
6194 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: this, args&: m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::ORDER, args&: m_SelectedEnvelope, args: m_SelectedEnvelope + 1));
6195 m_Map.SwapEnvelopes(Index0: m_SelectedEnvelope, Index1: m_SelectedEnvelope + 1);
6196 m_SelectedEnvelope = clamp<int>(val: m_SelectedEnvelope + 1, lo: 0, hi: m_Map.m_vpEnvelopes.size() - 1);
6197 pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
6198 m_Map.OnModify();
6199 }
6200
6201 // Move left button
6202 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
6203 static int s_MoveLeftButton = 0;
6204 if(DoButton_Ex(pId: &s_MoveLeftButton, pText: "←", Checked: 0, pRect: &Button, Flags: 0, pToolTip: "Move this envelope to the left", Corners: IGraphics::CORNER_L))
6205 {
6206 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: this, args&: m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::ORDER, args&: m_SelectedEnvelope, args: m_SelectedEnvelope - 1));
6207 m_Map.SwapEnvelopes(Index0: m_SelectedEnvelope - 1, Index1: m_SelectedEnvelope);
6208 m_SelectedEnvelope = clamp<int>(val: m_SelectedEnvelope - 1, lo: 0, hi: m_Map.m_vpEnvelopes.size() - 1);
6209 pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope];
6210 m_Map.OnModify();
6211 }
6212
6213 if(pEnvelope)
6214 {
6215 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
6216 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
6217 static int s_ZoomOutButton = 0;
6218 if(DoButton_FontIcon(pId: &s_ZoomOutButton, pText: FONT_ICON_MINUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad-] Zoom out horizontally, hold shift to zoom vertically", Corners: IGraphics::CORNER_R, FontSize: 9.0f))
6219 {
6220 if(Input()->ShiftIsPressed())
6221 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
6222 else
6223 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
6224 }
6225
6226 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
6227 static int s_ResetZoomButton = 0;
6228 if(DoButton_FontIcon(pId: &s_ResetZoomButton, pText: FONT_ICON_MAGNIFYING_GLASS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad*] Reset zoom to default value", Corners: IGraphics::CORNER_NONE, FontSize: 9.0f))
6229 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
6230
6231 ToolBar.VSplitRight(Cut: 20.0f, pLeft: &ToolBar, pRight: &Button);
6232 static int s_ZoomInButton = 0;
6233 if(DoButton_FontIcon(pId: &s_ZoomInButton, pText: FONT_ICON_PLUS, Checked: 0, pRect: &Button, Flags: 0, pToolTip: "[NumPad+] Zoom in horizontally, hold shift to zoom vertically", Corners: IGraphics::CORNER_L, FontSize: 9.0f))
6234 {
6235 if(Input()->ShiftIsPressed())
6236 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
6237 else
6238 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
6239 }
6240 }
6241
6242 // Margin on the right side
6243 ToolBar.VSplitRight(Cut: 7.0f, pLeft: &ToolBar, pRight: nullptr);
6244 }
6245
6246 if(pNewEnv) // add the default points
6247 {
6248 if(pNewEnv->GetChannels() == 4)
6249 {
6250 pNewEnv->AddPoint(Time: 0, v0: f2fx(v: 1.0f), v1: f2fx(v: 1.0f), v2: f2fx(v: 1.0f), v3: f2fx(v: 1.0f));
6251 pNewEnv->AddPoint(Time: 1000, v0: f2fx(v: 1.0f), v1: f2fx(v: 1.0f), v2: f2fx(v: 1.0f), v3: f2fx(v: 1.0f));
6252 }
6253 else
6254 {
6255 pNewEnv->AddPoint(Time: 0, v0: 0);
6256 pNewEnv->AddPoint(Time: 1000, v0: 0);
6257 }
6258
6259 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeAdd>(args: this, args&: pNewEnv));
6260 }
6261
6262 CUIRect Shifter, Inc, Dec;
6263 ToolBar.VSplitLeft(Cut: 60.0f, pLeft: &Shifter, pRight: &ToolBar);
6264 Shifter.VSplitRight(Cut: 15.0f, pLeft: &Shifter, pRight: &Inc);
6265 Shifter.VSplitLeft(Cut: 15.0f, pLeft: &Dec, pRight: &Shifter);
6266 char aBuf[64];
6267 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d/%d", m_SelectedEnvelope + 1, (int)m_Map.m_vpEnvelopes.size());
6268
6269 ColorRGBA EnvColor = ColorRGBA(1, 1, 1, 0.5f);
6270 if(!m_Map.m_vpEnvelopes.empty())
6271 {
6272 EnvColor = IsEnvelopeUsed(EnvelopeIndex: m_SelectedEnvelope) ? ColorRGBA(1, 0.7f, 0.7f, 0.5f) : ColorRGBA(0.7f, 1, 0.7f, 0.5f);
6273 }
6274
6275 static int s_EnvelopeSelector = 0;
6276 auto NewValueRes = UiDoValueSelector(pId: &s_EnvelopeSelector, pRect: &Shifter, pLabel: aBuf, Current: m_SelectedEnvelope + 1, Min: 1, Max: m_Map.m_vpEnvelopes.size(), Step: 1, Scale: 1.0f, pToolTip: "Select Envelope", IsDegree: false, IsHex: false, Corners: IGraphics::CORNER_NONE, pColor: &EnvColor, ShowValue: false);
6277 int NewValue = NewValueRes.m_Value;
6278 if(NewValue - 1 != m_SelectedEnvelope)
6279 {
6280 m_SelectedEnvelope = NewValue - 1;
6281 CurrentEnvelopeSwitched = true;
6282 }
6283
6284 static int s_PrevButton = 0;
6285 if(DoButton_FontIcon(pId: &s_PrevButton, pText: FONT_ICON_MINUS, Checked: 0, pRect: &Dec, Flags: 0, pToolTip: "Previous Envelope", Corners: IGraphics::CORNER_L, FontSize: 7.0f))
6286 {
6287 m_SelectedEnvelope--;
6288 if(m_SelectedEnvelope < 0)
6289 m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1;
6290 CurrentEnvelopeSwitched = true;
6291 }
6292
6293 static int s_NextButton = 0;
6294 if(DoButton_FontIcon(pId: &s_NextButton, pText: FONT_ICON_PLUS, Checked: 0, pRect: &Inc, Flags: 0, pToolTip: "Next Envelope", Corners: IGraphics::CORNER_R, FontSize: 7.0f))
6295 {
6296 m_SelectedEnvelope++;
6297 if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size())
6298 m_SelectedEnvelope = 0;
6299 CurrentEnvelopeSwitched = true;
6300 }
6301
6302 if(pEnvelope)
6303 {
6304 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
6305 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
6306 Ui()->DoLabel(pRect: &Button, pText: "Name:", Size: 10.0f, Align: TEXTALIGN_MR);
6307
6308 ToolBar.VSplitLeft(Cut: 3.0f, pLeft: nullptr, pRight: &ToolBar);
6309 ToolBar.VSplitLeft(Cut: ToolBar.w > ToolBar.h * 40 ? 80.0f : 60.0f, pLeft: &Button, pRight: &ToolBar);
6310
6311 s_NameInput.SetBuffer(pStr: pEnvelope->m_aName, MaxSize: sizeof(pEnvelope->m_aName));
6312 if(DoEditBox(pLineInput: &s_NameInput, pRect: &Button, FontSize: 10.0f, Corners: IGraphics::CORNER_ALL, pToolTip: "The name of the selected envelope"))
6313 {
6314 m_Map.OnModify();
6315 }
6316 }
6317 }
6318
6319 bool ShowColorBar = false;
6320 if(pEnvelope && pEnvelope->GetChannels() == 4)
6321 {
6322 ShowColorBar = true;
6323 View.HSplitTop(Cut: 20.0f, pTop: &ColorBar, pBottom: &View);
6324 ColorBar.Margin(Cut: 2.0f, pOtherRect: &ColorBar);
6325 }
6326
6327 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 0.1f);
6328
6329 if(pEnvelope)
6330 {
6331 if(m_ResetZoomEnvelope)
6332 {
6333 m_ResetZoomEnvelope = false;
6334 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
6335 }
6336
6337 ColorRGBA aColors[] = {ColorRGBA(1, 0.2f, 0.2f), ColorRGBA(0.2f, 1, 0.2f), ColorRGBA(0.2f, 0.2f, 1), ColorRGBA(1, 1, 0.2f)};
6338
6339 CUIRect Button;
6340
6341 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
6342
6343 static const char *s_aapNames[4][CEnvPoint::MAX_CHANNELS] = {
6344 {"V", "", "", ""},
6345 {"", "", "", ""},
6346 {"X", "Y", "R", ""},
6347 {"R", "G", "B", "A"},
6348 };
6349
6350 static const char *s_aapDescriptions[4][CEnvPoint::MAX_CHANNELS] = {
6351 {"Volume of the envelope", "", "", ""},
6352 {"", "", "", ""},
6353 {"X-axis of the envelope", "Y-axis of the envelope", "Rotation of the envelope", ""},
6354 {"Red value of the envelope", "Green value of the envelope", "Blue value of the envelope", "Alpha value of the envelope"},
6355 };
6356
6357 static int s_aChannelButtons[CEnvPoint::MAX_CHANNELS] = {0};
6358 int Bit = 1;
6359
6360 for(int i = 0; i < CEnvPoint::MAX_CHANNELS; i++, Bit <<= 1)
6361 {
6362 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: &Button, pRight: &ToolBar);
6363 if(i < pEnvelope->GetChannels())
6364 {
6365 int Corners = IGraphics::CORNER_NONE;
6366 if(pEnvelope->GetChannels() == 1)
6367 Corners = IGraphics::CORNER_ALL;
6368 else if(i == 0)
6369 Corners = IGraphics::CORNER_L;
6370 else if(i == pEnvelope->GetChannels() - 1)
6371 Corners = IGraphics::CORNER_R;
6372
6373 if(DoButton_Env(pId: &s_aChannelButtons[i], pText: s_aapNames[pEnvelope->GetChannels() - 1][i], Checked: s_ActiveChannels & Bit, pRect: &Button, pToolTip: s_aapDescriptions[pEnvelope->GetChannels() - 1][i], BaseColor: aColors[i], Corners))
6374 s_ActiveChannels ^= Bit;
6375 }
6376 }
6377
6378 // toggle sync button
6379 ToolBar.VSplitLeft(Cut: 15.0f, pLeft: nullptr, pRight: &ToolBar);
6380 ToolBar.VSplitLeft(Cut: 40.0f, pLeft: &Button, pRight: &ToolBar);
6381
6382 static int s_SyncButton;
6383 if(DoButton_Editor(pId: &s_SyncButton, pText: "Sync", Checked: pEnvelope->m_Synchronized, pRect: &Button, Flags: 0, pToolTip: "Synchronize envelope animation to game time (restarts when you touch the start line)"))
6384 {
6385 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEdit>(args: this, args&: m_SelectedEnvelope, args: CEditorActionEnvelopeEdit::EEditType::SYNC, args&: pEnvelope->m_Synchronized, args: !pEnvelope->m_Synchronized));
6386 pEnvelope->m_Synchronized = !pEnvelope->m_Synchronized;
6387 m_Map.OnModify();
6388 }
6389
6390 static int s_EnvelopeEditorId = 0;
6391 static int s_EnvelopeEditorButtonUsed = -1;
6392 const bool ShouldPan = s_Operation == EEnvelopeEditorOp::OP_NONE && (Ui()->MouseButton(Index: 2) || (Ui()->MouseButton(Index: 0) && Input()->ModifierIsPressed()));
6393 if(m_pContainerPanned == &s_EnvelopeEditorId)
6394 {
6395 if(!ShouldPan)
6396 m_pContainerPanned = nullptr;
6397 else
6398 {
6399 m_OffsetEnvelopeX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w;
6400 m_OffsetEnvelopeY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h;
6401 }
6402 }
6403
6404 if(Ui()->MouseInside(pRect: &View) && m_Dialog == DIALOG_NONE)
6405 {
6406 Ui()->SetHotItem(&s_EnvelopeEditorId);
6407
6408 if(ShouldPan && m_pContainerPanned == nullptr)
6409 m_pContainerPanned = &s_EnvelopeEditorId;
6410
6411 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
6412 ResetZoomEnvelope(pEnvelope, ActiveChannels: s_ActiveChannels);
6413 if(Input()->ShiftIsPressed())
6414 {
6415 if(Input()->KeyPress(Key: KEY_KP_MINUS))
6416 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
6417 if(Input()->KeyPress(Key: KEY_KP_PLUS))
6418 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
6419 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
6420 m_ZoomEnvelopeY.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeY.GetValue());
6421 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
6422 m_ZoomEnvelopeY.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeY.GetValue());
6423 }
6424 else
6425 {
6426 if(Input()->KeyPress(Key: KEY_KP_MINUS))
6427 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
6428 if(Input()->KeyPress(Key: KEY_KP_PLUS))
6429 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
6430 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
6431 m_ZoomEnvelopeX.ChangeValue(Amount: 0.1f * m_ZoomEnvelopeX.GetValue());
6432 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
6433 m_ZoomEnvelopeX.ChangeValue(Amount: -0.1f * m_ZoomEnvelopeX.GetValue());
6434 }
6435 }
6436
6437 if(Ui()->HotItem() == &s_EnvelopeEditorId)
6438 {
6439 // do stuff
6440 if(Ui()->MouseButton(Index: 0))
6441 {
6442 s_EnvelopeEditorButtonUsed = 0;
6443 if(s_Operation != EEnvelopeEditorOp::OP_BOX_SELECT && !Input()->ModifierIsPressed())
6444 {
6445 s_Operation = EEnvelopeEditorOp::OP_BOX_SELECT;
6446 s_MouseXStart = Ui()->MouseX();
6447 s_MouseYStart = Ui()->MouseY();
6448 }
6449 }
6450 else if(s_EnvelopeEditorButtonUsed == 0)
6451 {
6452 if(Ui()->DoDoubleClickLogic(pId: &s_EnvelopeEditorId))
6453 {
6454 // add point
6455 float Time = ScreenToEnvelopeX(View, x: Ui()->MouseX());
6456 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
6457 if(in_range(a: Time, lower: 0.0f, upper: pEnvelope->EndTime()))
6458 pEnvelope->Eval(Time, Result&: Channels, Channels: 4);
6459
6460 int FixedTime = std::round(x: Time * 1000.0f);
6461 bool TimeFound = false;
6462 for(CEnvPoint &Point : pEnvelope->m_vPoints)
6463 {
6464 if(Point.m_Time == FixedTime)
6465 TimeFound = true;
6466 }
6467
6468 if(!TimeFound)
6469 m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionAddEnvelopePoint>(args: this, args&: m_SelectedEnvelope, args&: FixedTime, args&: Channels));
6470
6471 if(FixedTime < 0)
6472 RemoveTimeOffsetEnvelope(pEnvelope);
6473 m_Map.OnModify();
6474 }
6475 s_EnvelopeEditorButtonUsed = -1;
6476 }
6477
6478 m_ShowEnvelopePreview = SHOWENV_SELECTED;
6479 str_copy(dst&: m_aTooltip, src: "Double-click to create a new point. Use shift to change the zoom axis. Press S to scale selected envelope points.");
6480 }
6481
6482 UpdateZoomEnvelopeX(View);
6483 UpdateZoomEnvelopeY(View);
6484
6485 {
6486 float UnitsPerLineY = 0.001f;
6487 static const float s_aUnitPerLineOptionsY[] = {0.005f, 0.01f, 0.025f, 0.05f, 0.1f, 0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f, 16.0f, 32.0f, 2 * 32.0f, 5 * 32.0f, 10 * 32.0f, 20 * 32.0f, 50 * 32.0f, 100 * 32.0f};
6488 for(float Value : s_aUnitPerLineOptionsY)
6489 {
6490 if(Value / m_ZoomEnvelopeY.GetValue() * View.h < 40.0f)
6491 UnitsPerLineY = Value;
6492 }
6493 int NumLinesY = m_ZoomEnvelopeY.GetValue() / UnitsPerLineY + 1;
6494
6495 Ui()->ClipEnable(pRect: &View);
6496 Graphics()->TextureClear();
6497 Graphics()->LinesBegin();
6498 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
6499
6500 float BaseValue = static_cast<int>(m_OffsetEnvelopeY * m_ZoomEnvelopeY.GetValue() / UnitsPerLineY) * UnitsPerLineY;
6501 for(int i = 0; i <= NumLinesY; i++)
6502 {
6503 float Value = UnitsPerLineY * i - BaseValue;
6504 IGraphics::CLineItem LineItem(View.x, EnvelopeToScreenY(View, y: Value), View.x + View.w, EnvelopeToScreenY(View, y: Value));
6505 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6506 }
6507
6508 Graphics()->LinesEnd();
6509
6510 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6511 for(int i = 0; i <= NumLinesY; i++)
6512 {
6513 float Value = UnitsPerLineY * i - BaseValue;
6514 char aValueBuffer[16];
6515 if(UnitsPerLineY >= 1.0f)
6516 {
6517 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%d", static_cast<int>(Value));
6518 }
6519 else
6520 {
6521 str_format(buffer: aValueBuffer, buffer_size: sizeof(aValueBuffer), format: "%.3f", Value);
6522 }
6523 Ui()->TextRender()->Text(x: View.x, y: EnvelopeToScreenY(View, y: Value) + 4.0f, Size: 8.0f, pText: aValueBuffer);
6524 }
6525 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
6526 Ui()->ClipDisable();
6527 }
6528
6529 {
6530 using namespace std::chrono_literals;
6531 CTimeStep UnitsPerLineX = 1ms;
6532 static const CTimeStep s_aUnitPerLineOptionsX[] = {5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2s, 5s, 10s, 15s, 30s, 1min};
6533 for(CTimeStep Value : s_aUnitPerLineOptionsX)
6534 {
6535 if(Value.AsSeconds() / m_ZoomEnvelopeX.GetValue() * View.w < 160.0f)
6536 UnitsPerLineX = Value;
6537 }
6538 int NumLinesX = m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds() + 1;
6539
6540 Ui()->ClipEnable(pRect: &View);
6541 Graphics()->TextureClear();
6542 Graphics()->LinesBegin();
6543 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.2f);
6544
6545 CTimeStep BaseValue = UnitsPerLineX * static_cast<int>(m_OffsetEnvelopeX * m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds());
6546 for(int i = 0; i <= NumLinesX; i++)
6547 {
6548 float Value = UnitsPerLineX.AsSeconds() * i - BaseValue.AsSeconds();
6549 IGraphics::CLineItem LineItem(EnvelopeToScreenX(View, x: Value), View.y, EnvelopeToScreenX(View, x: Value), View.y + View.h);
6550 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6551 }
6552
6553 Graphics()->LinesEnd();
6554
6555 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6556 for(int i = 0; i <= NumLinesX; i++)
6557 {
6558 CTimeStep Value = UnitsPerLineX * i - BaseValue;
6559 if(Value.AsSeconds() >= 0)
6560 {
6561 char aValueBuffer[16];
6562 Value.Format(pBuffer: aValueBuffer, BufferSize: sizeof(aValueBuffer));
6563
6564 Ui()->TextRender()->Text(x: EnvelopeToScreenX(View, x: Value.AsSeconds()) + 1.0f, y: View.y + View.h - 8.0f, Size: 8.0f, pText: aValueBuffer);
6565 }
6566 }
6567 Ui()->TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
6568 Ui()->ClipDisable();
6569 }
6570
6571 // render tangents for bezier curves
6572 {
6573 Ui()->ClipEnable(pRect: &View);
6574 Graphics()->TextureClear();
6575 Graphics()->LinesBegin();
6576 for(int c = 0; c < pEnvelope->GetChannels(); c++)
6577 {
6578 if(!(s_ActiveChannels & (1 << c)))
6579 continue;
6580
6581 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
6582 {
6583 float PosX = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6584 float PosY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
6585
6586 // Out-Tangent
6587 if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
6588 {
6589 float TangentX = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]));
6590 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
6591
6592 if(IsTangentOutPointSelected(Index: i, Channel: c))
6593 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6594 else
6595 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
6596
6597 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
6598 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6599 }
6600
6601 // In-Tangent
6602 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
6603 {
6604 float TangentX = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]));
6605 float TangentY = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
6606
6607 if(IsTangentInPointSelected(Index: i, Channel: c))
6608 Graphics()->SetColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.4f);
6609 else
6610 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 0.4f);
6611
6612 IGraphics::CLineItem LineItem(TangentX, TangentY, PosX, PosY);
6613 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6614 }
6615 }
6616 }
6617 Graphics()->LinesEnd();
6618 Ui()->ClipDisable();
6619 }
6620
6621 // render lines
6622 {
6623 float EndTimeTotal = maximum(a: 0.000001f, b: pEnvelope->EndTime());
6624 float EndX = clamp(val: EnvelopeToScreenX(View, x: EndTimeTotal), lo: View.x, hi: View.x + View.w);
6625 float StartX = clamp(val: View.x + View.w * m_OffsetEnvelopeX, lo: View.x, hi: View.x + View.w);
6626
6627 float EndTime = ScreenToEnvelopeX(View, x: EndX);
6628 float StartTime = ScreenToEnvelopeX(View, x: StartX);
6629
6630 Ui()->ClipEnable(pRect: &View);
6631 Graphics()->TextureClear();
6632 Graphics()->LinesBegin();
6633 for(int c = 0; c < pEnvelope->GetChannels(); c++)
6634 {
6635 if(s_ActiveChannels & (1 << c))
6636 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1);
6637 else
6638 Graphics()->SetColor(r: aColors[c].r * 0.5f, g: aColors[c].g * 0.5f, b: aColors[c].b * 0.5f, a: 1);
6639
6640 int Steps = static_cast<int>(((EndX - StartX) / Ui()->Screen()->w) * Graphics()->ScreenWidth());
6641 float StepTime = (EndTime - StartTime) / static_cast<float>(Steps);
6642 float StepSize = (EndX - StartX) / static_cast<float>(Steps);
6643
6644 ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
6645 pEnvelope->Eval(Time: StartTime, Result&: Channels, Channels: c + 1);
6646 float PrevY = EnvelopeToScreenY(View, y: Channels[c]);
6647 for(int i = 1; i < Steps; i++)
6648 {
6649 Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
6650 pEnvelope->Eval(Time: StartTime + i * StepTime, Result&: Channels, Channels: c + 1);
6651 float CurrentY = EnvelopeToScreenY(View, y: Channels[c]);
6652
6653 IGraphics::CLineItem LineItem(
6654 StartX + (i - 1) * StepSize,
6655 PrevY,
6656 StartX + i * StepSize,
6657 CurrentY);
6658 Graphics()->LinesDraw(pArray: &LineItem, Num: 1);
6659
6660 PrevY = CurrentY;
6661 }
6662 }
6663 Graphics()->LinesEnd();
6664 Ui()->ClipDisable();
6665 }
6666
6667 // render curve options
6668 {
6669 for(int i = 0; i < (int)pEnvelope->m_vPoints.size() - 1; i++)
6670 {
6671 float t0 = fxt2f(t: pEnvelope->m_vPoints[i].m_Time);
6672 float t1 = fxt2f(t: pEnvelope->m_vPoints[i + 1].m_Time);
6673
6674 CUIRect CurveButton;
6675 CurveButton.x = EnvelopeToScreenX(View, x: t0 + (t1 - t0) * 0.5f);
6676 CurveButton.y = CurveBar.y;
6677 CurveButton.h = CurveBar.h;
6678 CurveButton.w = CurveBar.h;
6679 CurveButton.x -= CurveButton.w / 2.0f;
6680 const void *pId = &pEnvelope->m_vPoints[i].m_Curvetype;
6681 const char *apTypeName[] = {"N", "L", "S", "F", "M", "B"};
6682 const char *pTypeName = "!?";
6683 if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(apTypeName))
6684 pTypeName = apTypeName[pEnvelope->m_vPoints[i].m_Curvetype];
6685
6686 if(CurveButton.x >= View.x)
6687 {
6688 if(DoButton_Editor(pId, pText: pTypeName, Checked: 0, pRect: &CurveButton, Flags: 0, pToolTip: "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)"))
6689 {
6690 int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype;
6691 pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES;
6692
6693 m_EnvelopeEditorHistory.RecordAction(pAction: std::make_shared<CEditorActionEnvelopeEditPoint>(args: this,
6694 args&: m_SelectedEnvelope, args&: i, args: 0, args: CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, args&: PrevCurve, args&: pEnvelope->m_vPoints[i].m_Curvetype));
6695 m_Map.OnModify();
6696 }
6697 }
6698 }
6699 }
6700
6701 // render colorbar
6702 if(ShowColorBar)
6703 {
6704 Ui()->ClipEnable(pRect: &ColorBar);
6705
6706 float StartX = maximum(a: EnvelopeToScreenX(View, x: 0), b: ColorBar.x);
6707 float EndX = EnvelopeToScreenX(View, x: pEnvelope->EndTime());
6708 CUIRect BackgroundView{
6709 .x: StartX,
6710 .y: ColorBar.y,
6711 .w: minimum(a: EndX - StartX, b: ColorBar.x + ColorBar.w - StartX),
6712 .h: ColorBar.h};
6713 RenderBackground(View: BackgroundView, Texture: m_CheckerTexture, Size: 16.0f, Brightness: 1.0f);
6714
6715 Graphics()->TextureClear();
6716 Graphics()->QuadsBegin();
6717 for(int i = 0; i < (int)pEnvelope->m_vPoints.size() - 1; i++)
6718 {
6719 float r0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[0]);
6720 float g0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[1]);
6721 float b0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[2]);
6722 float a0 = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[3]);
6723 float r1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[0]);
6724 float g1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[1]);
6725 float b1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[2]);
6726 float a1 = fx2f(v: pEnvelope->m_vPoints[i + 1].m_aValues[3]);
6727
6728 IGraphics::CColorVertex Array[4] = {IGraphics::CColorVertex(0, r0, g0, b0, a0),
6729 IGraphics::CColorVertex(1, r1, g1, b1, a1),
6730 IGraphics::CColorVertex(2, r1, g1, b1, a1),
6731 IGraphics::CColorVertex(3, r0, g0, b0, a0)};
6732 Graphics()->SetColorVertex(pArray: Array, Num: 4);
6733
6734 float x0 = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6735 float x1 = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i + 1].m_Time));
6736
6737 IGraphics::CQuadItem QuadItem(x0, ColorBar.y, x1 - x0, ColorBar.h);
6738 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6739 }
6740 Graphics()->QuadsEnd();
6741 Ui()->ClipDisable();
6742 }
6743
6744 // render handles
6745 if(CurrentEnvelopeSwitched)
6746 {
6747 DeselectEnvPoints();
6748 m_ResetZoomEnvelope = true;
6749 }
6750
6751 {
6752 static SPopupMenuId s_PopupEnvPointId;
6753 const auto &&ShowPopupEnvPoint = [&]() {
6754 Ui()->DoPopupMenu(pId: &s_PopupEnvPointId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 150, Height: 56 + (pEnvelope->GetChannels() == 4 && !IsTangentSelected() ? 16.0f : 0.0f), pContext: this, pfnFunc: PopupEnvPoint);
6755 };
6756
6757 if(s_Operation == EEnvelopeEditorOp::OP_NONE)
6758 {
6759 SetHotEnvelopePoint(View, pEnvelope, ActiveChannels: s_ActiveChannels);
6760 if(!Ui()->MouseButton(Index: 0))
6761 m_EnvOpTracker.Stop(Switch: false);
6762 }
6763 else
6764 {
6765 m_EnvOpTracker.Begin(Operation: s_Operation);
6766 }
6767
6768 Ui()->ClipEnable(pRect: &View);
6769 Graphics()->TextureClear();
6770 Graphics()->QuadsBegin();
6771 for(int c = 0; c < pEnvelope->GetChannels(); c++)
6772 {
6773 if(!(s_ActiveChannels & (1 << c)))
6774 continue;
6775
6776 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
6777 {
6778 // point handle
6779 {
6780 CUIRect Final;
6781 Final.x = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time));
6782 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]));
6783 Final.x -= 2.0f;
6784 Final.y -= 2.0f;
6785 Final.w = 4.0f;
6786 Final.h = 4.0f;
6787
6788 const void *pId = &pEnvelope->m_vPoints[i].m_aValues[c];
6789
6790 if(IsEnvPointSelected(Index: i, Channel: c))
6791 {
6792 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6793 CUIRect Background = {
6794 .x: Final.x - 0.2f * Final.w,
6795 .y: Final.y - 0.2f * Final.h,
6796 .w: Final.w * 1.4f,
6797 .h: Final.h * 1.4f};
6798 IGraphics::CQuadItem QuadItem(Background.x, Background.y, Background.w, Background.h);
6799 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6800 }
6801
6802 if(Ui()->CheckActiveItem(pId))
6803 {
6804 m_ShowEnvelopePreview = SHOWENV_SELECTED;
6805
6806 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
6807 {
6808 float dx = s_MouseXStart - Ui()->MouseX();
6809 float dy = s_MouseYStart - Ui()->MouseY();
6810
6811 if(dx * dx + dy * dy > 20.0f)
6812 {
6813 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT;
6814
6815 if(!IsEnvPointSelected(Index: i, Channel: c))
6816 SelectEnvPoint(Index: i, Channel: c);
6817 }
6818 }
6819
6820 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y)
6821 {
6822 if(Input()->ShiftIsPressed())
6823 {
6824 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y)
6825 {
6826 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_X;
6827 s_vAccurateDragValuesX.clear();
6828 for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints)
6829 s_vAccurateDragValuesX.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_Time);
6830 }
6831 else
6832 {
6833 float DeltaX = ScreenToEnvelopeDX(View, dx: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
6834
6835 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6836 {
6837 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
6838 int BoundLow = f2fxt(t: ScreenToEnvelopeX(View, x: View.x));
6839 int BoundHigh = f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w));
6840 for(int j = 0; j < SelectedIndex; j++)
6841 {
6842 if(!IsEnvPointSelected(Index: j))
6843 BoundLow = maximum(a: pEnvelope->m_vPoints[j].m_Time + 1, b: BoundLow);
6844 }
6845 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
6846 {
6847 if(!IsEnvPointSelected(Index: j))
6848 BoundHigh = minimum(a: pEnvelope->m_vPoints[j].m_Time - 1, b: BoundHigh);
6849 }
6850
6851 DeltaX = ClampDelta(Val: s_vAccurateDragValuesX[k], Delta: DeltaX, Min: BoundLow, Max: BoundHigh);
6852 }
6853 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6854 {
6855 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
6856 s_vAccurateDragValuesX[k] += DeltaX;
6857 pEnvelope->m_vPoints[SelectedIndex].m_Time = std::round(x: s_vAccurateDragValuesX[k]);
6858 }
6859 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6860 {
6861 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
6862 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != 0)
6863 {
6864 RemoveTimeOffsetEnvelope(pEnvelope);
6865 float Offset = s_vAccurateDragValuesX[k];
6866 for(auto &Value : s_vAccurateDragValuesX)
6867 Value -= Offset;
6868 break;
6869 }
6870 }
6871 }
6872 }
6873 else
6874 {
6875 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X)
6876 {
6877 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_Y;
6878 s_vAccurateDragValuesY.clear();
6879 for(auto [SelectedIndex, SelectedChannel] : m_vSelectedEnvelopePoints)
6880 s_vAccurateDragValuesY.push_back(x: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]);
6881 }
6882 else
6883 {
6884 float DeltaY = ScreenToEnvelopeDY(View, dy: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
6885 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
6886 {
6887 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k];
6888 s_vAccurateDragValuesY[k] -= DeltaY;
6889 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vAccurateDragValuesY[k]);
6890
6891 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
6892 {
6893 pEnvelope->m_vPoints[i].m_aValues[c] = clamp(val: pEnvelope->m_vPoints[i].m_aValues[c], lo: 0, hi: 1024);
6894 s_vAccurateDragValuesY[k] = clamp<float>(val: s_vAccurateDragValuesY[k], lo: 0, hi: 1024);
6895 }
6896 }
6897 }
6898 }
6899 }
6900
6901 if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU)
6902 {
6903 if(!Ui()->MouseButton(Index: 1))
6904 {
6905 if(m_vSelectedEnvelopePoints.size() == 1)
6906 {
6907 m_UpdateEnvPointInfo = true;
6908 ShowPopupEnvPoint();
6909 }
6910 else if(m_vSelectedEnvelopePoints.size() > 1)
6911 {
6912 static SPopupMenuId s_PopupEnvPointMultiId;
6913 Ui()->DoPopupMenu(pId: &s_PopupEnvPointMultiId, X: Ui()->MouseX(), Y: Ui()->MouseY(), Width: 80, Height: 22, pContext: this, pfnFunc: PopupEnvPointMulti);
6914 }
6915 Ui()->SetActiveItem(nullptr);
6916 s_Operation = EEnvelopeEditorOp::OP_NONE;
6917 }
6918 }
6919 else if(!Ui()->MouseButton(Index: 0))
6920 {
6921 Ui()->SetActiveItem(nullptr);
6922 m_SelectedQuadEnvelope = -1;
6923
6924 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
6925 {
6926 if(Input()->ShiftIsPressed())
6927 ToggleEnvPoint(Index: i, Channel: c);
6928 else
6929 SelectEnvPoint(Index: i, Channel: c);
6930 }
6931
6932 s_Operation = EEnvelopeEditorOp::OP_NONE;
6933 m_Map.OnModify();
6934 }
6935
6936 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6937 }
6938 else if(Ui()->HotItem() == pId)
6939 {
6940 if(Ui()->MouseButton(Index: 0))
6941 {
6942 Ui()->SetActiveItem(pId);
6943 s_Operation = EEnvelopeEditorOp::OP_SELECT;
6944 m_SelectedQuadEnvelope = m_SelectedEnvelope;
6945
6946 s_MouseXStart = Ui()->MouseX();
6947 s_MouseYStart = Ui()->MouseY();
6948 }
6949 else if(Ui()->MouseButtonClicked(Index: 1))
6950 {
6951 if(Input()->ShiftIsPressed())
6952 {
6953 m_EnvelopeEditorHistory.Execute(pAction: std::make_shared<CEditorActionDeleteEnvelopePoint>(args: this, args&: m_SelectedEnvelope, args&: i));
6954 }
6955 else
6956 {
6957 s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU;
6958 if(!IsEnvPointSelected(Index: i, Channel: c))
6959 SelectEnvPoint(Index: i, Channel: c);
6960 Ui()->SetActiveItem(pId);
6961 }
6962 }
6963
6964 m_ShowEnvelopePreview = SHOWENV_SELECTED;
6965 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6966 str_copy(dst&: m_aTooltip, src: "Envelope point. Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time. Shift + right-click to delete.");
6967 ms_pUiGotContext = pId;
6968 }
6969 else
6970 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
6971
6972 IGraphics::CQuadItem QuadItem(Final.x, Final.y, Final.w, Final.h);
6973 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
6974 }
6975
6976 // tangent handles for bezier curves
6977 if(i >= 0 && i < (int)pEnvelope->m_vPoints.size())
6978 {
6979 // Out-Tangent handle
6980 if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER)
6981 {
6982 CUIRect Final;
6983 Final.x = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]));
6984 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c]));
6985 Final.x -= 2.0f;
6986 Final.y -= 2.0f;
6987 Final.w = 4.0f;
6988 Final.h = 4.0f;
6989
6990 // handle logic
6991 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c];
6992
6993 if(IsTangentOutPointSelected(Index: i, Channel: c))
6994 {
6995 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
6996 IGraphics::CFreeformItem FreeformItem(
6997 Final.x + Final.w / 2.0f,
6998 Final.y - 1,
6999 Final.x + Final.w / 2.0f,
7000 Final.y - 1,
7001 Final.x + Final.w + 1,
7002 Final.y + Final.h + 1,
7003 Final.x - 1,
7004 Final.y + Final.h + 1);
7005 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7006 }
7007
7008 if(Ui()->CheckActiveItem(pId))
7009 {
7010 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7011
7012 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7013 {
7014 float dx = s_MouseXStart - Ui()->MouseX();
7015 float dy = s_MouseYStart - Ui()->MouseY();
7016
7017 if(dx * dx + dy * dy > 20.0f)
7018 {
7019 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT;
7020
7021 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c])};
7022 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c])};
7023
7024 if(!IsTangentOutPointSelected(Index: i, Channel: c))
7025 SelectTangentOutPoint(Index: i, Channel: c);
7026 }
7027 }
7028
7029 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT)
7030 {
7031 float DeltaX = ScreenToEnvelopeDX(View, dx: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
7032 float DeltaY = ScreenToEnvelopeDY(View, dy: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
7033 s_vAccurateDragValuesX[0] += DeltaX;
7034 s_vAccurateDragValuesY[0] -= DeltaY;
7035
7036 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = std::round(x: s_vAccurateDragValuesX[0]);
7037 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
7038
7039 // clamp time value
7040 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = clamp<int>(val: pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c], lo: 0, hi: f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time);
7041 s_vAccurateDragValuesX[0] = clamp<float>(val: s_vAccurateDragValuesX[0], lo: 0, hi: f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time);
7042 }
7043
7044 if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU)
7045 {
7046 if(!Ui()->MouseButton(Index: 1))
7047 {
7048 if(IsTangentOutPointSelected(Index: i, Channel: c))
7049 {
7050 m_UpdateEnvPointInfo = true;
7051 ShowPopupEnvPoint();
7052 }
7053 Ui()->SetActiveItem(nullptr);
7054 s_Operation = EEnvelopeEditorOp::OP_NONE;
7055 }
7056 }
7057 else if(!Ui()->MouseButton(Index: 0))
7058 {
7059 Ui()->SetActiveItem(nullptr);
7060 m_SelectedQuadEnvelope = -1;
7061
7062 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7063 SelectTangentOutPoint(Index: i, Channel: c);
7064
7065 s_Operation = EEnvelopeEditorOp::OP_NONE;
7066 m_Map.OnModify();
7067 }
7068
7069 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7070 }
7071 else if(Ui()->HotItem() == pId)
7072 {
7073 if(Ui()->MouseButton(Index: 0))
7074 {
7075 Ui()->SetActiveItem(pId);
7076 s_Operation = EEnvelopeEditorOp::OP_SELECT;
7077 m_SelectedQuadEnvelope = m_SelectedEnvelope;
7078
7079 s_MouseXStart = Ui()->MouseX();
7080 s_MouseYStart = Ui()->MouseY();
7081 }
7082 else if(Ui()->MouseButtonClicked(Index: 1))
7083 {
7084 if(Input()->ShiftIsPressed())
7085 {
7086 SelectTangentOutPoint(Index: i, Channel: c);
7087 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c] = 0.0f;
7088 pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c] = 0.0f;
7089 m_Map.OnModify();
7090 }
7091 else
7092 {
7093 s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU;
7094 SelectTangentOutPoint(Index: i, Channel: c);
7095 Ui()->SetActiveItem(pId);
7096 }
7097 }
7098
7099 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7100 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7101 str_copy(dst&: m_aTooltip, src: "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift + right-click to reset.");
7102 ms_pUiGotContext = pId;
7103 }
7104 else
7105 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
7106
7107 // draw triangle
7108 IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h);
7109 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7110 }
7111
7112 // In-Tangent handle
7113 if(i > 0 && pEnvelope->m_vPoints[i - 1].m_Curvetype == CURVETYPE_BEZIER)
7114 {
7115 CUIRect Final;
7116 Final.x = EnvelopeToScreenX(View, x: fxt2f(t: pEnvelope->m_vPoints[i].m_Time + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]));
7117 Final.y = EnvelopeToScreenY(View, y: fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c] + pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c]));
7118 Final.x -= 2.0f;
7119 Final.y -= 2.0f;
7120 Final.w = 4.0f;
7121 Final.h = 4.0f;
7122
7123 // handle logic
7124 const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c];
7125
7126 if(IsTangentInPointSelected(Index: i, Channel: c))
7127 {
7128 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7129 IGraphics::CFreeformItem FreeformItem(
7130 Final.x + Final.w / 2.0f,
7131 Final.y - 1,
7132 Final.x + Final.w / 2.0f,
7133 Final.y - 1,
7134 Final.x + Final.w + 1,
7135 Final.y + Final.h + 1,
7136 Final.x - 1,
7137 Final.y + Final.h + 1);
7138 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7139 }
7140
7141 if(Ui()->CheckActiveItem(pId))
7142 {
7143 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7144
7145 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7146 {
7147 float dx = s_MouseXStart - Ui()->MouseX();
7148 float dy = s_MouseYStart - Ui()->MouseY();
7149
7150 if(dx * dx + dy * dy > 20.0f)
7151 {
7152 s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT;
7153
7154 s_vAccurateDragValuesX = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c])};
7155 s_vAccurateDragValuesY = {static_cast<float>(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c])};
7156
7157 if(!IsTangentInPointSelected(Index: i, Channel: c))
7158 SelectTangentInPoint(Index: i, Channel: c);
7159 }
7160 }
7161
7162 if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT)
7163 {
7164 float DeltaX = ScreenToEnvelopeDX(View, dx: Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f);
7165 float DeltaY = ScreenToEnvelopeDY(View, dy: Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f);
7166 s_vAccurateDragValuesX[0] += DeltaX;
7167 s_vAccurateDragValuesY[0] -= DeltaY;
7168
7169 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = std::round(x: s_vAccurateDragValuesX[0]);
7170 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = std::round(x: s_vAccurateDragValuesY[0]);
7171
7172 // clamp time value
7173 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = clamp(val: pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c], lo: f2fxt(t: ScreenToEnvelopeX(View, x: View.x)) - pEnvelope->m_vPoints[i].m_Time, hi: 0);
7174 s_vAccurateDragValuesX[0] = clamp<float>(val: s_vAccurateDragValuesX[0], lo: f2fxt(t: ScreenToEnvelopeX(View, x: View.x)) - pEnvelope->m_vPoints[i].m_Time, hi: 0);
7175 }
7176
7177 if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU)
7178 {
7179 if(!Ui()->MouseButton(Index: 1))
7180 {
7181 if(IsTangentInPointSelected(Index: i, Channel: c))
7182 {
7183 m_UpdateEnvPointInfo = true;
7184 ShowPopupEnvPoint();
7185 }
7186 Ui()->SetActiveItem(nullptr);
7187 s_Operation = EEnvelopeEditorOp::OP_NONE;
7188 }
7189 }
7190 else if(!Ui()->MouseButton(Index: 0))
7191 {
7192 Ui()->SetActiveItem(nullptr);
7193 m_SelectedQuadEnvelope = -1;
7194
7195 if(s_Operation == EEnvelopeEditorOp::OP_SELECT)
7196 SelectTangentInPoint(Index: i, Channel: c);
7197
7198 s_Operation = EEnvelopeEditorOp::OP_NONE;
7199 m_Map.OnModify();
7200 }
7201
7202 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7203 }
7204 else if(Ui()->HotItem() == pId)
7205 {
7206 if(Ui()->MouseButton(Index: 0))
7207 {
7208 Ui()->SetActiveItem(pId);
7209 s_Operation = EEnvelopeEditorOp::OP_SELECT;
7210 m_SelectedQuadEnvelope = m_SelectedEnvelope;
7211
7212 s_MouseXStart = Ui()->MouseX();
7213 s_MouseYStart = Ui()->MouseY();
7214 }
7215 else if(Ui()->MouseButtonClicked(Index: 1))
7216 {
7217 if(Input()->ShiftIsPressed())
7218 {
7219 SelectTangentInPoint(Index: i, Channel: c);
7220 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c] = 0.0f;
7221 pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c] = 0.0f;
7222 m_Map.OnModify();
7223 }
7224 else
7225 {
7226 s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU;
7227 SelectTangentInPoint(Index: i, Channel: c);
7228 Ui()->SetActiveItem(pId);
7229 }
7230 }
7231
7232 m_ShowEnvelopePreview = SHOWENV_SELECTED;
7233 Graphics()->SetColor(r: 1, g: 1, b: 1, a: 1);
7234 str_copy(dst&: m_aTooltip, src: "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift + right-click to reset.");
7235 ms_pUiGotContext = pId;
7236 }
7237 else
7238 Graphics()->SetColor(r: aColors[c].r, g: aColors[c].g, b: aColors[c].b, a: 1.0f);
7239
7240 // draw triangle
7241 IGraphics::CFreeformItem FreeformItem(Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w / 2.0f, Final.y, Final.x + Final.w, Final.y + Final.h, Final.x, Final.y + Final.h);
7242 Graphics()->QuadsDrawFreeform(pArray: &FreeformItem, Num: 1);
7243 }
7244 }
7245 }
7246 }
7247 Graphics()->QuadsEnd();
7248 Ui()->ClipDisable();
7249 }
7250
7251 // handle scaling
7252 static float s_ScaleFactorX = 1.0f;
7253 static float s_ScaleFactorY = 1.0f;
7254 static float s_MidpointX = 0.0f;
7255 static float s_MidpointY = 0.0f;
7256 static std::vector<float> s_vInitialPositionsX;
7257 static std::vector<float> s_vInitialPositionsY;
7258 if(s_Operation == EEnvelopeEditorOp::OP_NONE && !s_NameInput.IsActive() && Input()->KeyIsPressed(Key: KEY_S) && !Input()->ModifierIsPressed() && !m_vSelectedEnvelopePoints.empty())
7259 {
7260 s_Operation = EEnvelopeEditorOp::OP_SCALE;
7261 s_ScaleFactorX = 1.0f;
7262 s_ScaleFactorY = 1.0f;
7263 auto [FirstPointIndex, FirstPointChannel] = m_vSelectedEnvelopePoints.front();
7264
7265 float MaximumX = pEnvelope->m_vPoints[FirstPointIndex].m_Time;
7266 float MinimumX = MaximumX;
7267 s_vInitialPositionsX.clear();
7268 for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints)
7269 {
7270 float Value = pEnvelope->m_vPoints[SelectedIndex].m_Time;
7271 s_vInitialPositionsX.push_back(x: Value);
7272 MaximumX = maximum(a: MaximumX, b: Value);
7273 MinimumX = minimum(a: MinimumX, b: Value);
7274 }
7275 s_MidpointX = (MaximumX - MinimumX) / 2.0f + MinimumX;
7276
7277 float MaximumY = pEnvelope->m_vPoints[FirstPointIndex].m_aValues[FirstPointChannel];
7278 float MinimumY = MaximumY;
7279 s_vInitialPositionsY.clear();
7280 for(auto [SelectedIndex, SelectedChannel] : m_vSelectedEnvelopePoints)
7281 {
7282 float Value = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel];
7283 s_vInitialPositionsY.push_back(x: Value);
7284 MaximumY = maximum(a: MaximumY, b: Value);
7285 MinimumY = minimum(a: MinimumY, b: Value);
7286 }
7287 s_MidpointY = (MaximumY - MinimumY) / 2.0f + MinimumY;
7288 }
7289
7290 if(s_Operation == EEnvelopeEditorOp::OP_SCALE)
7291 {
7292 str_copy(dst&: m_aTooltip, src: "Press shift to scale the time. Press alt to scale along midpoint. Press ctrl to be more precise.");
7293
7294 if(Input()->ShiftIsPressed())
7295 {
7296 s_ScaleFactorX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
7297 float Midpoint = Input()->AltIsPressed() ? s_MidpointX : 0.0f;
7298 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7299 {
7300 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
7301 int BoundLow = f2fxt(t: ScreenToEnvelopeX(View, x: View.x));
7302 int BoundHigh = f2fxt(t: ScreenToEnvelopeX(View, x: View.x + View.w));
7303 for(int j = 0; j < SelectedIndex; j++)
7304 {
7305 if(!IsEnvPointSelected(Index: j))
7306 BoundLow = maximum(a: pEnvelope->m_vPoints[j].m_Time + 1, b: BoundLow);
7307 }
7308 for(int j = SelectedIndex + 1; j < (int)pEnvelope->m_vPoints.size(); j++)
7309 {
7310 if(!IsEnvPointSelected(Index: j))
7311 BoundHigh = minimum(a: pEnvelope->m_vPoints[j].m_Time - 1, b: BoundHigh);
7312 }
7313
7314 float Value = s_vInitialPositionsX[k];
7315 float ScaleBoundLow = (BoundLow - Midpoint) / (Value - Midpoint);
7316 float ScaleBoundHigh = (BoundHigh - Midpoint) / (Value - Midpoint);
7317 float ScaleBoundMin = minimum(a: ScaleBoundLow, b: ScaleBoundHigh);
7318 float ScaleBoundMax = maximum(a: ScaleBoundLow, b: ScaleBoundHigh);
7319 s_ScaleFactorX = clamp(val: s_ScaleFactorX, lo: ScaleBoundMin, hi: ScaleBoundMax);
7320 }
7321
7322 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7323 {
7324 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
7325 float ScaleMinimum = s_vInitialPositionsX[k] - Midpoint > fxt2f(t: 1) ? fxt2f(t: 1) / (s_vInitialPositionsX[k] - Midpoint) : 0.0f;
7326 float ScaleFactor = maximum(a: ScaleMinimum, b: s_ScaleFactorX);
7327 pEnvelope->m_vPoints[SelectedIndex].m_Time = std::round(x: (s_vInitialPositionsX[k] - Midpoint) * ScaleFactor + Midpoint);
7328 }
7329 for(size_t k = 1; k < pEnvelope->m_vPoints.size(); k++)
7330 {
7331 if(pEnvelope->m_vPoints[k].m_Time <= pEnvelope->m_vPoints[k - 1].m_Time)
7332 pEnvelope->m_vPoints[k].m_Time = pEnvelope->m_vPoints[k - 1].m_Time + 1;
7333 }
7334 for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints)
7335 {
7336 if(SelectedIndex == 0 && pEnvelope->m_vPoints[SelectedIndex].m_Time != 0)
7337 {
7338 float Offset = pEnvelope->m_vPoints[0].m_Time;
7339 RemoveTimeOffsetEnvelope(pEnvelope);
7340 s_MidpointX -= Offset;
7341 for(auto &Value : s_vInitialPositionsX)
7342 Value -= Offset;
7343 break;
7344 }
7345 }
7346 }
7347 else
7348 {
7349 s_ScaleFactorY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f);
7350 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7351 {
7352 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k];
7353 if(Input()->AltIsPressed())
7354 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: (s_vInitialPositionsY[k] - s_MidpointY) * s_ScaleFactorY + s_MidpointY);
7355 else
7356 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k] * s_ScaleFactorY);
7357
7358 if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4)
7359 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = clamp(val: pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel], lo: 0, hi: 1024);
7360 }
7361 }
7362
7363 if(Ui()->MouseButton(Index: 0))
7364 {
7365 s_Operation = EEnvelopeEditorOp::OP_NONE;
7366 m_EnvOpTracker.Stop(Switch: false);
7367 }
7368 else if(Ui()->MouseButton(Index: 1) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
7369 {
7370 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7371 {
7372 int SelectedIndex = m_vSelectedEnvelopePoints[k].first;
7373 pEnvelope->m_vPoints[SelectedIndex].m_Time = std::round(x: s_vInitialPositionsX[k]);
7374 }
7375 for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++)
7376 {
7377 auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k];
7378 pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(x: s_vInitialPositionsY[k]);
7379 }
7380 RemoveTimeOffsetEnvelope(pEnvelope);
7381 s_Operation = EEnvelopeEditorOp::OP_NONE;
7382 }
7383 }
7384
7385 // handle box selection
7386 if(s_Operation == EEnvelopeEditorOp::OP_BOX_SELECT)
7387 {
7388 IGraphics::CLineItem aLines[4] = {
7389 {s_MouseXStart, s_MouseYStart, Ui()->MouseX(), s_MouseYStart},
7390 {s_MouseXStart, s_MouseYStart, s_MouseXStart, Ui()->MouseY()},
7391 {s_MouseXStart, Ui()->MouseY(), Ui()->MouseX(), Ui()->MouseY()},
7392 {Ui()->MouseX(), s_MouseYStart, Ui()->MouseX(), Ui()->MouseY()}};
7393 Ui()->ClipEnable(pRect: &View);
7394 Graphics()->LinesBegin();
7395 Graphics()->LinesDraw(pArray: aLines, Num: std::size(aLines));
7396 Graphics()->LinesEnd();
7397 Ui()->ClipDisable();
7398
7399 if(!Ui()->MouseButton(Index: 0))
7400 {
7401 s_Operation = EEnvelopeEditorOp::OP_NONE;
7402 Ui()->SetActiveItem(nullptr);
7403
7404 float TimeStart = ScreenToEnvelopeX(View, x: s_MouseXStart);
7405 float TimeEnd = ScreenToEnvelopeX(View, x: Ui()->MouseX());
7406 float ValueStart = ScreenToEnvelopeY(View, y: s_MouseYStart);
7407 float ValueEnd = ScreenToEnvelopeY(View, y: Ui()->MouseY());
7408
7409 float TimeMin = minimum(a: TimeStart, b: TimeEnd);
7410 float TimeMax = maximum(a: TimeStart, b: TimeEnd);
7411 float ValueMin = minimum(a: ValueStart, b: ValueEnd);
7412 float ValueMax = maximum(a: ValueStart, b: ValueEnd);
7413
7414 if(!Input()->ShiftIsPressed())
7415 DeselectEnvPoints();
7416
7417 for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++)
7418 {
7419 for(int c = 0; c < CEnvPoint::MAX_CHANNELS; c++)
7420 {
7421 if(!(s_ActiveChannels & (1 << c)))
7422 continue;
7423
7424 float Time = fxt2f(t: pEnvelope->m_vPoints[i].m_Time);
7425 float Value = fx2f(v: pEnvelope->m_vPoints[i].m_aValues[c]);
7426
7427 if(in_range(a: Time, lower: TimeMin, upper: TimeMax) && in_range(a: Value, lower: ValueMin, upper: ValueMax))
7428 ToggleEnvPoint(Index: i, Channel: c);
7429 }
7430 }
7431 }
7432 }
7433 }
7434}
7435
7436void CEditor::RenderEditorHistory(CUIRect View)
7437{
7438 enum EHistoryType
7439 {
7440 EDITOR_HISTORY,
7441 ENVELOPE_HISTORY,
7442 SERVER_SETTINGS_HISTORY
7443 };
7444
7445 static EHistoryType s_HistoryType = EDITOR_HISTORY;
7446 static int s_ActionSelectedIndex = 0;
7447 static CListBox s_ListBox;
7448 s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen());
7449
7450 const bool GotSelection = s_ListBox.Active() && s_ActionSelectedIndex >= 0 && (size_t)s_ActionSelectedIndex < m_Map.m_vSettings.size();
7451
7452 CUIRect ToolBar, Button, Label, List, DragBar;
7453 View.HSplitTop(Cut: 22.0f, pTop: &DragBar, pBottom: nullptr);
7454 DragBar.y -= 2.0f;
7455 DragBar.w += 2.0f;
7456 DragBar.h += 4.0f;
7457 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::SIDE_TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_HISTORY]);
7458 View.HSplitTop(Cut: 20.0f, pTop: &ToolBar, pBottom: &View);
7459 View.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &List);
7460 ToolBar.HMargin(Cut: 2.0f, pOtherRect: &ToolBar);
7461
7462 CUIRect TypeButtons, HistoryTypeButton;
7463 const int HistoryTypeBtnSize = 70.0f;
7464 ToolBar.VSplitLeft(Cut: 3 * HistoryTypeBtnSize, pLeft: &TypeButtons, pRight: &Label);
7465
7466 // history type buttons
7467 {
7468 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
7469 static int s_EditorHistoryButton = 0;
7470 if(DoButton_Ex(pId: &s_EditorHistoryButton, pText: "Editor", Checked: s_HistoryType == EDITOR_HISTORY, pRect: &HistoryTypeButton, Flags: 0, pToolTip: "Show map editor history.", Corners: IGraphics::CORNER_L))
7471 {
7472 s_HistoryType = EDITOR_HISTORY;
7473 }
7474
7475 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
7476 static int s_EnvelopeEditorHistoryButton = 0;
7477 if(DoButton_Ex(pId: &s_EnvelopeEditorHistoryButton, pText: "Envelope", Checked: s_HistoryType == ENVELOPE_HISTORY, pRect: &HistoryTypeButton, Flags: 0, pToolTip: "Show envelope editor history.", Corners: IGraphics::CORNER_NONE))
7478 {
7479 s_HistoryType = ENVELOPE_HISTORY;
7480 }
7481
7482 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
7483 static int s_ServerSettingsHistoryButton = 0;
7484 if(DoButton_Ex(pId: &s_ServerSettingsHistoryButton, pText: "Settings", Checked: s_HistoryType == SERVER_SETTINGS_HISTORY, pRect: &HistoryTypeButton, Flags: 0, pToolTip: "Show server settings editor history.", Corners: IGraphics::CORNER_R))
7485 {
7486 s_HistoryType = SERVER_SETTINGS_HISTORY;
7487 }
7488 }
7489
7490 SLabelProperties InfoProps;
7491 InfoProps.m_MaxWidth = ToolBar.w - 60.f;
7492 InfoProps.m_EllipsisAtEnd = true;
7493 Label.VSplitLeft(Cut: 8.0f, pLeft: nullptr, pRight: &Label);
7494 Ui()->DoLabel(pRect: &Label, pText: "Editor history. Click on an action to undo all actions above.", Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: InfoProps);
7495
7496 CEditorHistory *pCurrentHistory;
7497 if(s_HistoryType == EDITOR_HISTORY)
7498 pCurrentHistory = &m_EditorHistory;
7499 else if(s_HistoryType == ENVELOPE_HISTORY)
7500 pCurrentHistory = &m_EnvelopeEditorHistory;
7501 else if(s_HistoryType == SERVER_SETTINGS_HISTORY)
7502 pCurrentHistory = &m_ServerSettingsHistory;
7503 else
7504 return;
7505
7506 // delete button
7507 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
7508 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
7509 static int s_DeleteButton = 0;
7510 if(DoButton_FontIcon(pId: &s_DeleteButton, pText: FONT_ICON_TRASH, Checked: (!pCurrentHistory->m_vpUndoActions.empty() || !pCurrentHistory->m_vpRedoActions.empty()) ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "Clear the history.", Corners: IGraphics::CORNER_ALL, FontSize: 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
7511 {
7512 pCurrentHistory->Clear();
7513 s_ActionSelectedIndex = 0;
7514 }
7515
7516 // actions list
7517 int RedoSize = (int)pCurrentHistory->m_vpRedoActions.size();
7518 int UndoSize = (int)pCurrentHistory->m_vpUndoActions.size();
7519 s_ActionSelectedIndex = RedoSize;
7520 s_ListBox.DoStart(RowHeight: 15.0f, NumItems: RedoSize + UndoSize, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_ActionSelectedIndex, pRect: &List);
7521
7522 for(int i = 0; i < RedoSize; i++)
7523 {
7524 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pCurrentHistory->m_vpRedoActions[i], Selected: s_ActionSelectedIndex >= 0 && s_ActionSelectedIndex == i);
7525 if(!Item.m_Visible)
7526 continue;
7527
7528 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
7529
7530 SLabelProperties Props;
7531 Props.m_MaxWidth = Label.w;
7532 Props.m_EllipsisAtEnd = true;
7533 TextRender()->TextColor(rgb: {.5f, .5f, .5f});
7534 TextRender()->TextOutlineColor(rgb: TextRender()->DefaultTextOutlineColor());
7535 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpRedoActions[i]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
7536 TextRender()->TextColor(rgb: TextRender()->DefaultTextColor());
7537 }
7538
7539 for(int i = 0; i < UndoSize; i++)
7540 {
7541 const CListboxItem Item = s_ListBox.DoNextItem(pId: &pCurrentHistory->m_vpUndoActions[UndoSize - i - 1], Selected: s_ActionSelectedIndex >= RedoSize && s_ActionSelectedIndex == (i + RedoSize));
7542 if(!Item.m_Visible)
7543 continue;
7544
7545 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
7546
7547 SLabelProperties Props;
7548 Props.m_MaxWidth = Label.w;
7549 Props.m_EllipsisAtEnd = true;
7550 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
7551 }
7552
7553 { // Base action "Loaded map" that cannot be undone
7554 static int s_BaseAction;
7555 const CListboxItem Item = s_ListBox.DoNextItem(pId: &s_BaseAction, Selected: s_ActionSelectedIndex == RedoSize + UndoSize);
7556 if(Item.m_Visible)
7557 {
7558 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
7559
7560 Ui()->DoLabel(pRect: &Label, pText: "Loaded map", Size: 10.0f, Align: TEXTALIGN_ML);
7561 }
7562 }
7563
7564 const int NewSelected = s_ListBox.DoEnd();
7565 if(s_ActionSelectedIndex != NewSelected)
7566 {
7567 // Figure out if we should undo or redo some actions
7568 // Undo everything until the selected index
7569 if(NewSelected > s_ActionSelectedIndex)
7570 {
7571 for(int i = 0; i < (NewSelected - s_ActionSelectedIndex); i++)
7572 {
7573 pCurrentHistory->Undo();
7574 }
7575 }
7576 else
7577 {
7578 for(int i = 0; i < (s_ActionSelectedIndex - NewSelected); i++)
7579 {
7580 pCurrentHistory->Redo();
7581 }
7582 }
7583 s_ActionSelectedIndex = NewSelected;
7584 }
7585}
7586
7587void CEditor::DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue, float MaxValue)
7588{
7589 enum EDragOperation
7590 {
7591 OP_NONE,
7592 OP_DRAGGING,
7593 OP_CLICKED
7594 };
7595 static EDragOperation s_Operation = OP_NONE;
7596 static float s_InitialMouseY = 0.0f;
7597 static float s_InitialMouseOffsetY = 0.0f;
7598 static float s_InitialMouseX = 0.0f;
7599 static float s_InitialMouseOffsetX = 0.0f;
7600
7601 bool IsVertical = Side == EDragSide::SIDE_TOP || Side == EDragSide::SIDE_BOTTOM;
7602
7603 if(Ui()->MouseInside(pRect: pDragBar) && Ui()->HotItem() == pDragBar)
7604 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
7605
7606 bool Clicked;
7607 bool Abrupted;
7608 if(int Result = DoButton_DraggableEx(pId: pDragBar, pText: "", Checked: 8, pRect: pDragBar, pClicked: &Clicked, pAbrupted: &Abrupted, Flags: 0, pToolTip: "Change the size of the editor by dragging."))
7609 {
7610 if(s_Operation == OP_NONE && Result == 1)
7611 {
7612 s_InitialMouseY = Ui()->MouseY();
7613 s_InitialMouseOffsetY = Ui()->MouseY() - pDragBar->y;
7614 s_InitialMouseX = Ui()->MouseX();
7615 s_InitialMouseOffsetX = Ui()->MouseX() - pDragBar->x;
7616 s_Operation = OP_CLICKED;
7617 }
7618
7619 if(Clicked || Abrupted)
7620 s_Operation = OP_NONE;
7621
7622 if(s_Operation == OP_CLICKED && absolute(a: IsVertical ? Ui()->MouseY() - s_InitialMouseY : Ui()->MouseX() - s_InitialMouseX) > 5.0f)
7623 s_Operation = OP_DRAGGING;
7624
7625 if(s_Operation == OP_DRAGGING)
7626 {
7627 if(Side == EDragSide::SIDE_TOP)
7628 *pValue = clamp(val: s_InitialMouseOffsetY + View.y + View.h - Ui()->MouseY(), lo: MinValue, hi: MaxValue);
7629 else if(Side == EDragSide::SIDE_RIGHT)
7630 *pValue = clamp(val: Ui()->MouseX() - s_InitialMouseOffsetX - View.x + pDragBar->w, lo: MinValue, hi: MaxValue);
7631 else if(Side == EDragSide::SIDE_BOTTOM)
7632 *pValue = clamp(val: Ui()->MouseY() - s_InitialMouseOffsetY - View.y + pDragBar->h, lo: MinValue, hi: MaxValue);
7633 else if(Side == EDragSide::SIDE_LEFT)
7634 *pValue = clamp(val: s_InitialMouseOffsetX + View.x + View.w - Ui()->MouseX(), lo: MinValue, hi: MaxValue);
7635
7636 m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H;
7637 }
7638 }
7639}
7640
7641void CEditor::RenderMenubar(CUIRect MenuBar)
7642{
7643 SPopupMenuProperties PopupProperties;
7644 PopupProperties.m_Corners = IGraphics::CORNER_R | IGraphics::CORNER_B;
7645
7646 CUIRect FileButton;
7647 static int s_FileButton = 0;
7648 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &FileButton, pRight: &MenuBar);
7649 if(DoButton_Ex(pId: &s_FileButton, pText: "File", Checked: 0, pRect: &FileButton, Flags: 0, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
7650 {
7651 static SPopupMenuId s_PopupMenuFileId;
7652 Ui()->DoPopupMenu(pId: &s_PopupMenuFileId, X: FileButton.x, Y: FileButton.y + FileButton.h - 1.0f, Width: 120.0f, Height: 174.0f, pContext: this, pfnFunc: PopupMenuFile, Props: PopupProperties);
7653 }
7654
7655 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
7656
7657 CUIRect ToolsButton;
7658 static int s_ToolsButton = 0;
7659 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &ToolsButton, pRight: &MenuBar);
7660 if(DoButton_Ex(pId: &s_ToolsButton, pText: "Tools", Checked: 0, pRect: &ToolsButton, Flags: 0, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
7661 {
7662 static SPopupMenuId s_PopupMenuToolsId;
7663 Ui()->DoPopupMenu(pId: &s_PopupMenuToolsId, X: ToolsButton.x, Y: ToolsButton.y + ToolsButton.h - 1.0f, Width: 200.0f, Height: 64.0f, pContext: this, pfnFunc: PopupMenuTools, Props: PopupProperties);
7664 }
7665
7666 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
7667
7668 CUIRect SettingsButton;
7669 static int s_SettingsButton = 0;
7670 MenuBar.VSplitLeft(Cut: 60.0f, pLeft: &SettingsButton, pRight: &MenuBar);
7671 if(DoButton_Ex(pId: &s_SettingsButton, pText: "Settings", Checked: 0, pRect: &SettingsButton, Flags: 0, pToolTip: nullptr, Corners: IGraphics::CORNER_T, FontSize: EditorFontSizes::MENU, Align: TEXTALIGN_ML))
7672 {
7673 static SPopupMenuId s_PopupMenuEntitiesId;
7674 Ui()->DoPopupMenu(pId: &s_PopupMenuEntitiesId, X: SettingsButton.x, Y: SettingsButton.y + SettingsButton.h - 1.0f, Width: 200.0f, Height: 92.0f, pContext: this, pfnFunc: PopupMenuSettings, Props: PopupProperties);
7675 }
7676
7677 CUIRect ChangedIndicator, Info, Help, Close;
7678 MenuBar.VSplitLeft(Cut: 5.0f, pLeft: nullptr, pRight: &MenuBar);
7679 MenuBar.VSplitLeft(Cut: MenuBar.h, pLeft: &ChangedIndicator, pRight: &MenuBar);
7680 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Close);
7681 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
7682 MenuBar.VSplitRight(Cut: 15.0f, pLeft: &MenuBar, pRight: &Help);
7683 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
7684 MenuBar.VSplitLeft(Cut: MenuBar.w * 0.6f, pLeft: &MenuBar, pRight: &Info);
7685 MenuBar.VSplitRight(Cut: 5.0f, pLeft: &MenuBar, pRight: nullptr);
7686
7687 if(m_Map.m_Modified)
7688 {
7689 TextRender()->SetFontPreset(EFontPreset::ICON_FONT);
7690 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);
7691 Ui()->DoLabel(pRect: &ChangedIndicator, pText: FONT_ICON_CIRCLE, Size: 8.0f, Align: TEXTALIGN_MC);
7692 TextRender()->SetRenderFlags(0);
7693 TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT);
7694 static int s_ChangedIndicator;
7695 DoButton_Editor_Common(pId: &s_ChangedIndicator, pText: "", Checked: 0, pRect: &ChangedIndicator, Flags: 0, pToolTip: "This map has unsaved changes"); // just for the tooltip, result unused
7696 }
7697
7698 char aBuf[IO_MAX_PATH_LENGTH + 32];
7699 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "File: %s", m_aFileName);
7700 SLabelProperties Props;
7701 Props.m_MaxWidth = MenuBar.w;
7702 Props.m_EllipsisAtEnd = true;
7703 Ui()->DoLabel(pRect: &MenuBar, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
7704
7705 char aTimeStr[6];
7706 str_timestamp_format(buffer: aTimeStr, buffer_size: sizeof(aTimeStr), format: "%H:%M");
7707
7708 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "X: %.1f, Y: %.1f, Z: %.1f, A: %.1f, G: %i %s", Ui()->MouseWorldX() / 32.0f, Ui()->MouseWorldY() / 32.0f, MapView()->Zoom()->GetValue(), m_AnimateSpeed, MapView()->MapGrid()->Factor(), aTimeStr);
7709 Ui()->DoLabel(pRect: &Info, pText: aBuf, Size: 10.0f, Align: TEXTALIGN_MR);
7710
7711 static int s_HelpButton = 0;
7712 if(DoButton_Editor(pId: &s_HelpButton, pText: "?", Checked: 0, pRect: &Help, Flags: 0, pToolTip: "[F1] Open the DDNet Wiki page for the Map Editor in a web browser") || (Input()->KeyPress(Key: KEY_F1) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr))
7713 {
7714 const char *pLink = Localize(pStr: "https://wiki.ddnet.org/wiki/Mapping");
7715 if(!Client()->ViewLink(pLink))
7716 {
7717 ShowFileDialogError(pFormat: "Failed to open the link '%s' in the default web browser.", pLink);
7718 }
7719 }
7720
7721 static int s_CloseButton = 0;
7722 if(DoButton_Editor(pId: &s_CloseButton, pText: "×", Checked: 0, pRect: &Close, Flags: 0, pToolTip: "Exits from the editor"))
7723 {
7724 OnClose();
7725 g_Config.m_ClEditor = 0;
7726 }
7727}
7728
7729void CEditor::Render()
7730{
7731 // basic start
7732 Graphics()->Clear(r: 0.0f, g: 0.0f, b: 0.0f);
7733 CUIRect View = *Ui()->Screen();
7734 Ui()->MapScreen();
7735 m_CursorType = CURSOR_NORMAL;
7736
7737 float Width = View.w;
7738 float Height = View.h;
7739
7740 // reset tip
7741 str_copy(dst&: m_aTooltip, src: "");
7742
7743 // render checker
7744 RenderBackground(View, Texture: m_CheckerTexture, Size: 32.0f, Brightness: 1.0f);
7745
7746 CUIRect MenuBar, ModeBar, ToolBar, StatusBar, ExtraEditor, ToolBox;
7747 m_ShowPicker = Input()->KeyIsPressed(Key: KEY_SPACE) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && m_vSelectedLayers.size() == 1;
7748
7749 if(m_GuiActive)
7750 {
7751 View.HSplitTop(Cut: 16.0f, pTop: &MenuBar, pBottom: &View);
7752 View.HSplitTop(Cut: 53.0f, pTop: &ToolBar, pBottom: &View);
7753 View.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ToolBox, pRight: &View);
7754
7755 View.HSplitBottom(Cut: 16.0f, pTop: &View, pBottom: &StatusBar);
7756 if(!m_ShowPicker && m_ActiveExtraEditor != EXTRAEDITOR_NONE)
7757 View.HSplitBottom(Cut: m_aExtraEditorSplits[(int)m_ActiveExtraEditor], pTop: &View, pBottom: &ExtraEditor);
7758 }
7759 else
7760 {
7761 // hack to get keyboard inputs from toolbar even when GUI is not active
7762 ToolBar.x = -100;
7763 ToolBar.y = -100;
7764 ToolBar.w = 50;
7765 ToolBar.h = 50;
7766 }
7767
7768 // a little hack for now
7769 if(m_Mode == MODE_LAYERS)
7770 DoMapEditor(View);
7771
7772 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
7773 {
7774 // handle undo/redo hotkeys
7775 if(Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && !Input()->ShiftIsPressed())
7776 UndoLastAction();
7777 if((Input()->KeyPress(Key: KEY_Y) && Input()->ModifierIsPressed()) || (Input()->KeyPress(Key: KEY_Z) && Input()->ModifierIsPressed() && Input()->ShiftIsPressed()))
7778 RedoLastAction();
7779
7780 // handle brush save/load hotkeys
7781 for(int i = KEY_1; i <= KEY_0; i++)
7782 {
7783 if(Input()->KeyPress(Key: i))
7784 {
7785 int Slot = i - KEY_1;
7786 if(Input()->ModifierIsPressed() && !m_pBrush->IsEmpty())
7787 {
7788 dbg_msg(sys: "editor", fmt: "saving current brush to %d", Slot);
7789 m_apSavedBrushes[Slot] = std::make_shared<CLayerGroup>(args&: *m_pBrush);
7790 }
7791 else if(m_apSavedBrushes[Slot])
7792 {
7793 dbg_msg(sys: "editor", fmt: "loading brush from slot %d", Slot);
7794 m_pBrush = std::make_shared<CLayerGroup>(args&: *m_apSavedBrushes[Slot]);
7795 }
7796 }
7797 }
7798 }
7799
7800 float Brightness = 0.25f;
7801
7802 if(m_GuiActive)
7803 {
7804 RenderBackground(View: MenuBar, Texture: m_BackgroundTexture, Size: 128.0f, Brightness: Brightness * 0);
7805 MenuBar.Margin(Cut: 2.0f, pOtherRect: &MenuBar);
7806
7807 RenderBackground(View: ToolBox, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7808 ToolBox.Margin(Cut: 2.0f, pOtherRect: &ToolBox);
7809
7810 RenderBackground(View: ToolBar, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7811 ToolBar.Margin(Cut: 2.0f, pOtherRect: &ToolBar);
7812 ToolBar.VSplitLeft(Cut: m_ToolBoxWidth, pLeft: &ModeBar, pRight: &ToolBar);
7813
7814 RenderBackground(View: StatusBar, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7815 StatusBar.Margin(Cut: 2.0f, pOtherRect: &StatusBar);
7816 }
7817
7818 // do the toolbar
7819 if(m_Mode == MODE_LAYERS)
7820 DoToolbarLayers(ToolBar);
7821 else if(m_Mode == MODE_SOUNDS)
7822 DoToolbarSounds(ToolBar);
7823
7824 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)
7825 {
7826 const bool ModPressed = Input()->ModifierIsPressed();
7827 const bool ShiftPressed = Input()->ShiftIsPressed();
7828 const bool AltPressed = Input()->AltIsPressed();
7829 // ctrl+n to create new map
7830 if(Input()->KeyPress(Key: KEY_N) && ModPressed)
7831 {
7832 if(HasUnsavedData())
7833 {
7834 if(!m_PopupEventWasActivated)
7835 {
7836 m_PopupEventType = POPEVENT_NEW;
7837 m_PopupEventActivated = true;
7838 }
7839 }
7840 else
7841 {
7842 Reset();
7843 m_aFileName[0] = 0;
7844 }
7845 }
7846 // ctrl+a to append map
7847 if(Input()->KeyPress(Key: KEY_A) && ModPressed)
7848 {
7849 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_MAP, pTitle: "Append map", pButtonText: "Append", pBasePath: "maps", FilenameAsDefault: false, pfnFunc: CallbackAppendMap, pUser: this);
7850 }
7851 // ctrl+o or ctrl+l to open
7852 if((Input()->KeyPress(Key: KEY_O) || Input()->KeyPress(Key: KEY_L)) && ModPressed)
7853 {
7854 if(ShiftPressed)
7855 {
7856 if(HasUnsavedData())
7857 {
7858 if(!m_PopupEventWasActivated)
7859 {
7860 m_PopupEventType = POPEVENT_LOADCURRENT;
7861 m_PopupEventActivated = true;
7862 }
7863 }
7864 else
7865 {
7866 LoadCurrentMap();
7867 }
7868 }
7869 else
7870 {
7871 if(HasUnsavedData())
7872 {
7873 if(!m_PopupEventWasActivated)
7874 {
7875 m_PopupEventType = POPEVENT_LOAD;
7876 m_PopupEventActivated = true;
7877 }
7878 }
7879 else
7880 {
7881 InvokeFileDialog(StorageType: IStorage::TYPE_ALL, FileType: FILETYPE_MAP, pTitle: "Load map", pButtonText: "Load", pBasePath: "maps", FilenameAsDefault: false, pfnFunc: CallbackOpenMap, pUser: this);
7882 }
7883 }
7884 }
7885
7886 // ctrl+shift+alt+s to save copy
7887 if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed && AltPressed)
7888 InvokeFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: FILETYPE_MAP, pTitle: "Save map", pButtonText: "Save", pBasePath: "maps", FilenameAsDefault: true, pfnFunc: CallbackSaveCopyMap, pUser: this);
7889 // ctrl+shift+s to save as
7890 else if(Input()->KeyPress(Key: KEY_S) && ModPressed && ShiftPressed)
7891 InvokeFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: FILETYPE_MAP, pTitle: "Save map", pButtonText: "Save", pBasePath: "maps", FilenameAsDefault: true, pfnFunc: CallbackSaveMap, pUser: this);
7892 // ctrl+s to save
7893 else if(Input()->KeyPress(Key: KEY_S) && ModPressed)
7894 {
7895 if(m_aFileName[0] && m_ValidSaveFilename)
7896 {
7897 if(!m_PopupEventWasActivated)
7898 {
7899 str_copy(dst&: m_aFileSaveName, src: m_aFileName);
7900 CallbackSaveMap(pFileName: m_aFileSaveName, StorageType: IStorage::TYPE_SAVE, pUser: this);
7901 }
7902 }
7903 else
7904 InvokeFileDialog(StorageType: IStorage::TYPE_SAVE, FileType: FILETYPE_MAP, pTitle: "Save map", pButtonText: "Save", pBasePath: "maps", FilenameAsDefault: false, pfnFunc: CallbackSaveMap, pUser: this);
7905 }
7906 }
7907
7908 if(m_GuiActive)
7909 {
7910 CUIRect DragBar;
7911 ToolBox.VSplitRight(Cut: 1.0f, pLeft: &ToolBox, pRight: &DragBar);
7912 DragBar.x -= 2.0f;
7913 DragBar.w += 4.0f;
7914 DoEditorDragBar(View: ToolBox, pDragBar: &DragBar, Side: EDragSide::SIDE_RIGHT, pValue: &m_ToolBoxWidth);
7915
7916 if(m_Mode == MODE_LAYERS)
7917 RenderLayers(LayersBox: ToolBox);
7918 else if(m_Mode == MODE_IMAGES)
7919 {
7920 RenderImagesList(ToolBox);
7921 RenderSelectedImage(View);
7922 }
7923 else if(m_Mode == MODE_SOUNDS)
7924 RenderSounds(ToolBox);
7925 }
7926
7927 Ui()->MapScreen();
7928
7929 CUIRect TooltipRect;
7930 if(m_GuiActive)
7931 {
7932 RenderMenubar(MenuBar);
7933 RenderModebar(View: ModeBar);
7934 if(!m_ShowPicker)
7935 {
7936 if(m_ActiveExtraEditor != EXTRAEDITOR_NONE)
7937 {
7938 RenderBackground(View: ExtraEditor, Texture: m_BackgroundTexture, Size: 128.0f, Brightness);
7939 ExtraEditor.HMargin(Cut: 2.0f, pOtherRect: &ExtraEditor);
7940 ExtraEditor.VSplitRight(Cut: 2.0f, pLeft: &ExtraEditor, pRight: nullptr);
7941 }
7942
7943 static bool s_ShowServerSettingsEditorLast = false;
7944 if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
7945 {
7946 RenderEnvelopeEditor(View: ExtraEditor);
7947 }
7948 else if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
7949 {
7950 RenderServerSettingsEditor(View: ExtraEditor, ShowServerSettingsEditorLast: s_ShowServerSettingsEditorLast);
7951 }
7952 else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY)
7953 {
7954 RenderEditorHistory(View: ExtraEditor);
7955 }
7956 s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS;
7957 }
7958 RenderStatusbar(View: StatusBar, pTooltipRect: &TooltipRect);
7959 }
7960
7961 RenderPressedKeys(View);
7962 RenderSavingIndicator(View);
7963
7964 if(m_Dialog == DIALOG_FILE)
7965 {
7966 static int s_NullUiTarget = 0;
7967 Ui()->SetHotItem(&s_NullUiTarget);
7968 RenderFileDialog();
7969 }
7970 else if(m_Dialog == DIALOG_MAPSETTINGS_ERROR)
7971 {
7972 static int s_NullUiTarget = 0;
7973 Ui()->SetHotItem(&s_NullUiTarget);
7974 RenderMapSettingsErrorDialog();
7975 }
7976
7977 if(m_PopupEventActivated)
7978 {
7979 static SPopupMenuId s_PopupEventId;
7980 constexpr float PopupWidth = 400.0f;
7981 constexpr float PopupHeight = 150.0f;
7982 Ui()->DoPopupMenu(pId: &s_PopupEventId, X: Width / 2.0f - PopupWidth / 2.0f, Y: Height / 2.0f - PopupHeight / 2.0f, Width: PopupWidth, Height: PopupHeight, pContext: this, pfnFunc: PopupEvent);
7983 m_PopupEventActivated = false;
7984 m_PopupEventWasActivated = true;
7985 }
7986
7987 if(m_Dialog == DIALOG_NONE && !Ui()->IsPopupHovered() && Ui()->MouseInside(pRect: &View))
7988 {
7989 // handle zoom hotkeys
7990 if(Input()->KeyPress(Key: KEY_KP_MINUS))
7991 MapView()->Zoom()->ChangeValue(Amount: 50.0f);
7992 if(Input()->KeyPress(Key: KEY_KP_PLUS))
7993 MapView()->Zoom()->ChangeValue(Amount: -50.0f);
7994 if(Input()->KeyPress(Key: KEY_KP_MULTIPLY))
7995 MapView()->ResetZoom();
7996
7997 if(m_pBrush->IsEmpty() || !Input()->ShiftIsPressed())
7998 {
7999 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
8000 MapView()->Zoom()->ChangeValue(Amount: 20.0f);
8001 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
8002 MapView()->Zoom()->ChangeValue(Amount: -20.0f);
8003 }
8004 if(!m_pBrush->IsEmpty())
8005 {
8006 const bool HasTeleTiles = std::any_of(first: m_pBrush->m_vpLayers.begin(), last: m_pBrush->m_vpLayers.end(), pred: [](auto pLayer) {
8007 return pLayer->m_Type == LAYERTYPE_TILES && std::static_pointer_cast<CLayerTiles>(pLayer)->m_Tele;
8008 });
8009 if(HasTeleTiles)
8010 str_copy(dst&: m_aTooltip, src: "Use shift+mousewheel up/down to adjust the tele numbers. Use ctrl+f to change all tele numbers to the first unused number.");
8011
8012 if(Input()->ShiftIsPressed())
8013 {
8014 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_DOWN))
8015 AdjustBrushSpecialTiles(UseNextFree: false, Adjust: -1);
8016 if(Input()->KeyPress(Key: KEY_MOUSE_WHEEL_UP))
8017 AdjustBrushSpecialTiles(UseNextFree: false, Adjust: 1);
8018 }
8019
8020 // Use ctrl+f to replace number in brush with next free
8021 if(Input()->ModifierIsPressed() && Input()->KeyPress(Key: KEY_F))
8022 AdjustBrushSpecialTiles(UseNextFree: true);
8023 }
8024 }
8025
8026 for(CEditorComponent &Component : m_vComponents)
8027 Component.OnRender(View);
8028
8029 MapView()->UpdateZoom();
8030
8031 // Cancel color pipette with escape before closing popup menus with escape
8032 if(m_ColorPipetteActive && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
8033 {
8034 m_ColorPipetteActive = false;
8035 }
8036
8037 Ui()->RenderPopupMenus();
8038 FreeDynamicPopupMenus();
8039
8040 UpdateColorPipette();
8041
8042 if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))
8043 {
8044 OnClose();
8045 g_Config.m_ClEditor = 0;
8046 }
8047
8048 // The tooltip can be set in popup menus so we have to render the tooltip after the popup menus.
8049 if(m_GuiActive)
8050 RenderTooltip(TooltipRect);
8051
8052 RenderMousePointer();
8053}
8054
8055void CEditor::RenderPressedKeys(CUIRect View)
8056{
8057 if(!g_Config.m_EdShowkeys)
8058 return;
8059
8060 Ui()->MapScreen();
8061 CTextCursor Cursor;
8062 TextRender()->SetCursor(pCursor: &Cursor, x: View.x + 10, y: View.y + View.h - 24 - 10, FontSize: 24.0f, Flags: TEXTFLAG_RENDER);
8063
8064 int NKeys = 0;
8065 for(int i = 0; i < KEY_LAST; i++)
8066 {
8067 if(Input()->KeyIsPressed(Key: i))
8068 {
8069 if(NKeys)
8070 TextRender()->TextEx(pCursor: &Cursor, pText: " + ", Length: -1);
8071 TextRender()->TextEx(pCursor: &Cursor, pText: Input()->KeyName(Key: i), Length: -1);
8072 NKeys++;
8073 }
8074 }
8075}
8076
8077void CEditor::RenderSavingIndicator(CUIRect View)
8078{
8079 if(m_WriterFinishJobs.empty())
8080 return;
8081
8082 const char *pText = "Saving…";
8083 const float FontSize = 24.0f;
8084
8085 Ui()->MapScreen();
8086 CUIRect Label, Spinner;
8087 View.Margin(Cut: 20.0f, pOtherRect: &View);
8088 View.HSplitBottom(Cut: FontSize, pTop: nullptr, pBottom: &View);
8089 View.VSplitRight(Cut: TextRender()->TextWidth(Size: FontSize, pText) + 2.0f, pLeft: &Spinner, pRight: &Label);
8090 Spinner.VSplitRight(Cut: Spinner.h, pLeft: nullptr, pRight: &Spinner);
8091 Ui()->DoLabel(pRect: &Label, pText, Size: FontSize, Align: TEXTALIGN_MR);
8092 Ui()->RenderProgressSpinner(Center: Spinner.Center(), OuterRadius: 8.0f);
8093}
8094
8095void CEditor::FreeDynamicPopupMenus()
8096{
8097 auto Iterator = m_PopupMessageContexts.begin();
8098 while(Iterator != m_PopupMessageContexts.end())
8099 {
8100 if(!Ui()->IsPopupOpen(pId: Iterator->second))
8101 {
8102 CUi::SMessagePopupContext *pContext = Iterator->second;
8103 Iterator = m_PopupMessageContexts.erase(position: Iterator);
8104 delete pContext;
8105 }
8106 else
8107 ++Iterator;
8108 }
8109}
8110
8111void CEditor::UpdateColorPipette()
8112{
8113 if(!m_ColorPipetteActive)
8114 return;
8115
8116 static char s_PipetteScreenButton;
8117 if(Ui()->HotItem() == &s_PipetteScreenButton)
8118 {
8119 // Read color one pixel to the top and left as we would otherwise not read the correct
8120 // color due to the cursor sprite being rendered over the current mouse position.
8121 const int PixelX = clamp<int>(val: round_to_int(f: (Ui()->MouseX() - 1.0f) / Ui()->Screen()->w * Graphics()->ScreenWidth()), lo: 0, hi: Graphics()->ScreenWidth() - 1);
8122 const int PixelY = clamp<int>(val: round_to_int(f: (Ui()->MouseY() - 1.0f) / Ui()->Screen()->h * Graphics()->ScreenHeight()), lo: 0, hi: Graphics()->ScreenHeight() - 1);
8123 Graphics()->ReadPixel(Position: ivec2(PixelX, PixelY), pColor: &m_PipetteColor);
8124 }
8125
8126 // Simulate button overlaying the entire screen to intercept all clicks for color pipette.
8127 const int ButtonResult = DoButton_Editor_Common(pId: &s_PipetteScreenButton, pText: "", Checked: 0, pRect: Ui()->Screen(), Flags: 0, pToolTip: "Left click to pick a color from the screen. Right click to cancel pipette mode.");
8128 // Don't handle clicks if we are panning, so the pipette stays active while panning.
8129 // Checking m_pContainerPanned alone is not enough, as this variable is reset when
8130 // panning ends before this function is called.
8131 if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr)
8132 {
8133 if(ButtonResult == 1)
8134 {
8135 char aClipboard[9];
8136 str_format(buffer: aClipboard, buffer_size: sizeof(aClipboard), format: "%08X", m_PipetteColor.PackAlphaLast());
8137 Input()->SetClipboardText(aClipboard);
8138
8139 // Check if any of the saved colors is equal to the picked color and
8140 // bring it to the front of the list instead of adding a duplicate.
8141 int ShiftEnd = (int)std::size(m_aSavedColors) - 1;
8142 for(int i = 0; i < (int)std::size(m_aSavedColors); ++i)
8143 {
8144 if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack())
8145 {
8146 ShiftEnd = i;
8147 break;
8148 }
8149 }
8150 for(int i = ShiftEnd; i > 0; --i)
8151 {
8152 m_aSavedColors[i] = m_aSavedColors[i - 1];
8153 }
8154 m_aSavedColors[0] = m_PipetteColor;
8155 }
8156 if(ButtonResult > 0)
8157 {
8158 m_ColorPipetteActive = false;
8159 }
8160 }
8161}
8162
8163void CEditor::RenderMousePointer()
8164{
8165 if(!m_ShowMousePointer)
8166 return;
8167
8168 constexpr float CursorSize = 16.0f;
8169
8170 // Cursor
8171 Graphics()->WrapClamp();
8172 Graphics()->TextureSet(Texture: m_aCursorTextures[m_CursorType]);
8173 Graphics()->QuadsBegin();
8174 if(m_CursorType == CURSOR_RESIZE_V)
8175 {
8176 Graphics()->QuadsSetRotation(Angle: pi / 2.0f);
8177 }
8178 if(ms_pUiGotContext == Ui()->HotItem())
8179 {
8180 Graphics()->SetColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f);
8181 }
8182 const float CursorOffset = m_CursorType == CURSOR_RESIZE_V || m_CursorType == CURSOR_RESIZE_H ? -CursorSize / 2.0f : 0.0f;
8183 IGraphics::CQuadItem QuadItem(Ui()->MouseX() + CursorOffset, Ui()->MouseY() + CursorOffset, CursorSize, CursorSize);
8184 Graphics()->QuadsDrawTL(pArray: &QuadItem, Num: 1);
8185 Graphics()->QuadsEnd();
8186 Graphics()->WrapNormal();
8187
8188 // Pipette color
8189 if(m_ColorPipetteActive)
8190 {
8191 CUIRect PipetteRect = {.x: Ui()->MouseX() + CursorSize, .y: Ui()->MouseY() + CursorSize, .w: 80.0f, .h: 20.0f};
8192 if(PipetteRect.x + PipetteRect.w + 2.0f > Ui()->Screen()->w)
8193 {
8194 PipetteRect.x = Ui()->MouseX() - PipetteRect.w - CursorSize / 2.0f;
8195 }
8196 if(PipetteRect.y + PipetteRect.h + 2.0f > Ui()->Screen()->h)
8197 {
8198 PipetteRect.y = Ui()->MouseY() - PipetteRect.h - CursorSize / 2.0f;
8199 }
8200 PipetteRect.Draw(Color: ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
8201
8202 CUIRect Pipette, Label;
8203 PipetteRect.VSplitLeft(Cut: PipetteRect.h, pLeft: &Pipette, pRight: &Label);
8204 Pipette.Margin(Cut: 2.0f, pOtherRect: &Pipette);
8205 Pipette.Draw(Color: m_PipetteColor, Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
8206
8207 char aLabel[8];
8208 str_format(buffer: aLabel, buffer_size: sizeof(aLabel), format: "#%06X", m_PipetteColor.PackAlphaLast(Alpha: false));
8209 Ui()->DoLabel(pRect: &Label, pText: aLabel, Size: 10.0f, Align: TEXTALIGN_MC);
8210 }
8211}
8212
8213void CEditor::Reset(bool CreateDefault)
8214{
8215 Ui()->ClosePopupMenus();
8216 m_Map.Clean();
8217
8218 for(CEditorComponent &Component : m_vComponents)
8219 Component.OnReset();
8220
8221 // create default layers
8222 if(CreateDefault)
8223 {
8224 m_EditorWasUsedBefore = true;
8225 m_Map.CreateDefault(EntitiesTexture: GetEntitiesTexture());
8226 }
8227
8228 SelectGameLayer();
8229 DeselectQuads();
8230 DeselectQuadPoints();
8231 m_SelectedEnvelope = 0;
8232 m_SelectedImage = 0;
8233 m_SelectedSound = 0;
8234 m_SelectedSource = -1;
8235
8236 m_pContainerPanned = nullptr;
8237 m_pContainerPannedLast = nullptr;
8238
8239 m_Map.m_Modified = false;
8240 m_Map.m_ModifiedAuto = false;
8241 m_Map.m_LastModifiedTime = -1.0f;
8242 m_Map.m_LastSaveTime = Client()->GlobalTime();
8243 m_Map.m_LastAutosaveUpdateTime = -1.0f;
8244
8245 m_ShowEnvelopePreview = SHOWENV_NONE;
8246 m_ShiftBy = 1;
8247
8248 m_ResetZoomEnvelope = true;
8249 m_SettingsCommandInput.Clear();
8250
8251 m_EditorHistory.Clear();
8252 m_EnvelopeEditorHistory.Clear();
8253 m_ServerSettingsHistory.Clear();
8254
8255 m_QuadTracker.m_pEditor = this;
8256
8257 m_EnvOpTracker.m_pEditor = this;
8258 m_EnvOpTracker.Reset();
8259
8260 m_MapSettingsCommandContext.Reset();
8261}
8262
8263int CEditor::GetTextureUsageFlag() const
8264{
8265 return Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE;
8266}
8267
8268IGraphics::CTextureHandle CEditor::GetFrontTexture()
8269{
8270 if(!m_FrontTexture.IsValid())
8271 m_FrontTexture = Graphics()->LoadTexture(pFilename: "editor/front.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8272 return m_FrontTexture;
8273}
8274
8275IGraphics::CTextureHandle CEditor::GetTeleTexture()
8276{
8277 if(!m_TeleTexture.IsValid())
8278 m_TeleTexture = Graphics()->LoadTexture(pFilename: "editor/tele.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8279 return m_TeleTexture;
8280}
8281
8282IGraphics::CTextureHandle CEditor::GetSpeedupTexture()
8283{
8284 if(!m_SpeedupTexture.IsValid())
8285 m_SpeedupTexture = Graphics()->LoadTexture(pFilename: "editor/speedup.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8286 return m_SpeedupTexture;
8287}
8288
8289IGraphics::CTextureHandle CEditor::GetSwitchTexture()
8290{
8291 if(!m_SwitchTexture.IsValid())
8292 m_SwitchTexture = Graphics()->LoadTexture(pFilename: "editor/switch.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8293 return m_SwitchTexture;
8294}
8295
8296IGraphics::CTextureHandle CEditor::GetTuneTexture()
8297{
8298 if(!m_TuneTexture.IsValid())
8299 m_TuneTexture = Graphics()->LoadTexture(pFilename: "editor/tune.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8300 return m_TuneTexture;
8301}
8302
8303IGraphics::CTextureHandle CEditor::GetEntitiesTexture()
8304{
8305 if(!m_EntitiesTexture.IsValid())
8306 m_EntitiesTexture = Graphics()->LoadTexture(pFilename: "editor/entities/DDNet.png", StorageType: IStorage::TYPE_ALL, Flags: GetTextureUsageFlag());
8307 return m_EntitiesTexture;
8308}
8309
8310void CEditor::Init()
8311{
8312 m_pInput = Kernel()->RequestInterface<IInput>();
8313 m_pClient = Kernel()->RequestInterface<IClient>();
8314 m_pConfigManager = Kernel()->RequestInterface<IConfigManager>();
8315 m_pConfig = m_pConfigManager->Values();
8316 m_pConsole = Kernel()->RequestInterface<IConsole>();
8317 m_pEngine = Kernel()->RequestInterface<IEngine>();
8318 m_pGraphics = Kernel()->RequestInterface<IGraphics>();
8319 m_pTextRender = Kernel()->RequestInterface<ITextRender>();
8320 m_pStorage = Kernel()->RequestInterface<IStorage>();
8321 m_pSound = Kernel()->RequestInterface<ISound>();
8322 m_UI.Init(pKernel: Kernel());
8323 m_UI.SetPopupMenuClosedCallback([this]() {
8324 m_PopupEventWasActivated = false;
8325 });
8326 m_RenderTools.Init(pGraphics: m_pGraphics, pTextRender: m_pTextRender);
8327 m_ZoomEnvelopeX.Init(pEditor: this);
8328 m_ZoomEnvelopeY.Init(pEditor: this);
8329 m_Map.m_pEditor = this;
8330
8331 m_vComponents.emplace_back(args&: m_MapView);
8332 m_vComponents.emplace_back(args&: m_MapSettingsBackend);
8333 m_vComponents.emplace_back(args&: m_LayerSelector);
8334 for(CEditorComponent &Component : m_vComponents)
8335 Component.Init(pEditor: this);
8336
8337 m_CheckerTexture = Graphics()->LoadTexture(pFilename: "editor/checker.png", StorageType: IStorage::TYPE_ALL);
8338 m_BackgroundTexture = Graphics()->LoadTexture(pFilename: "editor/background.png", StorageType: IStorage::TYPE_ALL);
8339 m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture(pFilename: "editor/cursor.png", StorageType: IStorage::TYPE_ALL);
8340 m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture(pFilename: "editor/cursor_resize.png", StorageType: IStorage::TYPE_ALL);
8341 m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H];
8342
8343 m_pTilesetPicker = std::make_shared<CLayerTiles>(args: this, args: 16, args: 16);
8344 m_pTilesetPicker->MakePalette();
8345 m_pTilesetPicker->m_Readonly = true;
8346
8347 m_pQuadsetPicker = std::make_shared<CLayerQuads>(args: this);
8348 m_pQuadsetPicker->NewQuad(x: 0, y: 0, Width: 64, Height: 64);
8349 m_pQuadsetPicker->m_Readonly = true;
8350
8351 m_pBrush = std::make_shared<CLayerGroup>();
8352 m_pBrush->m_pMap = &m_Map;
8353
8354 Reset(CreateDefault: false);
8355}
8356
8357void CEditor::PlaceBorderTiles()
8358{
8359 std::shared_ptr<CLayerTiles> pT = std::static_pointer_cast<CLayerTiles>(r: GetSelectedLayerType(Index: 0, Type: LAYERTYPE_TILES));
8360
8361 for(int i = 0; i < pT->m_Width * pT->m_Height; ++i)
8362 {
8363 if(i % pT->m_Width < 2 || i % pT->m_Width > pT->m_Width - 3 || i < pT->m_Width * 2 || i > pT->m_Width * (pT->m_Height - 2))
8364 {
8365 int x = i % pT->m_Width;
8366 int y = i / pT->m_Width;
8367
8368 CTile Current = pT->m_pTiles[i];
8369 Current.m_Index = 1;
8370 pT->SetTile(x, y, Tile: Current);
8371 }
8372 }
8373
8374 int GameGroupIndex = std::find(first: m_Map.m_vpGroups.begin(), last: m_Map.m_vpGroups.end(), val: m_Map.m_pGameGroup) - m_Map.m_vpGroups.begin();
8375 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorBrushDrawAction>(args: this, args&: GameGroupIndex), pDisplay: "Tool 'Make borders'");
8376
8377 m_Map.OnModify();
8378}
8379
8380void CEditor::HandleCursorMovement()
8381{
8382 const vec2 UpdatedMousePos = Ui()->UpdatedMousePos();
8383 const vec2 UpdatedMouseDelta = Ui()->UpdatedMouseDelta();
8384
8385 // fix correct world x and y
8386 const std::shared_ptr<CLayerGroup> pGroup = GetSelectedGroup();
8387 if(pGroup)
8388 {
8389 float aPoints[4];
8390 pGroup->Mapping(pPoints: aPoints);
8391
8392 float WorldWidth = aPoints[2] - aPoints[0];
8393 float WorldHeight = aPoints[3] - aPoints[1];
8394
8395 m_MouseWorldScale = WorldWidth / Graphics()->WindowWidth();
8396
8397 m_MouseWorldPos.x = aPoints[0] + WorldWidth * (UpdatedMousePos.x / Graphics()->WindowWidth());
8398 m_MouseWorldPos.y = aPoints[1] + WorldHeight * (UpdatedMousePos.y / Graphics()->WindowHeight());
8399 m_MouseDeltaWorld.x = UpdatedMouseDelta.x * (WorldWidth / Graphics()->WindowWidth());
8400 m_MouseDeltaWorld.y = UpdatedMouseDelta.y * (WorldHeight / Graphics()->WindowHeight());
8401 }
8402 else
8403 {
8404 m_MouseWorldPos = vec2(-1.0f, -1.0f);
8405 m_MouseDeltaWorld = vec2(0.0f, 0.0f);
8406 }
8407
8408 m_MouseWorldNoParaPos = vec2(-1.0f, -1.0f);
8409 for(const std::shared_ptr<CLayerGroup> &pGameGroup : m_Map.m_vpGroups)
8410 {
8411 if(!pGameGroup->m_GameGroup)
8412 continue;
8413
8414 float aPoints[4];
8415 pGameGroup->Mapping(pPoints: aPoints);
8416
8417 float WorldWidth = aPoints[2] - aPoints[0];
8418 float WorldHeight = aPoints[3] - aPoints[1];
8419
8420 m_MouseWorldNoParaPos.x = aPoints[0] + WorldWidth * (UpdatedMousePos.x / Graphics()->WindowWidth());
8421 m_MouseWorldNoParaPos.y = aPoints[1] + WorldHeight * (UpdatedMousePos.y / Graphics()->WindowHeight());
8422 }
8423
8424 OnMouseMove(MousePos: UpdatedMousePos);
8425}
8426
8427void CEditor::OnMouseMove(vec2 MousePos)
8428{
8429 m_vHoverTiles.clear();
8430 for(size_t g = 0; g < m_Map.m_vpGroups.size(); g++)
8431 {
8432 const std::shared_ptr<CLayerGroup> pGroup = m_Map.m_vpGroups[g];
8433 for(size_t l = 0; l < pGroup->m_vpLayers.size(); l++)
8434 {
8435 const std::shared_ptr<CLayer> pLayer = pGroup->m_vpLayers[l];
8436 int LayerType = pLayer->m_Type;
8437 if(LayerType != LAYERTYPE_TILES &&
8438 LayerType != LAYERTYPE_FRONT &&
8439 LayerType != LAYERTYPE_TELE &&
8440 LayerType != LAYERTYPE_SPEEDUP &&
8441 LayerType != LAYERTYPE_SWITCH &&
8442 LayerType != LAYERTYPE_TUNE)
8443 continue;
8444
8445 std::shared_ptr<CLayerTiles> pTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
8446 pGroup->MapScreen();
8447 float aPoints[4];
8448 pGroup->Mapping(pPoints: aPoints);
8449 float WorldWidth = aPoints[2] - aPoints[0];
8450 float WorldHeight = aPoints[3] - aPoints[1];
8451 CUIRect Rect;
8452 Rect.x = aPoints[0] + WorldWidth * (MousePos.x / Graphics()->WindowWidth());
8453 Rect.y = aPoints[1] + WorldHeight * (MousePos.y / Graphics()->WindowHeight());
8454 Rect.w = 0;
8455 Rect.h = 0;
8456 RECTi r;
8457 pTiles->Convert(Rect, pOut: &r);
8458 pTiles->Clamp(pRect: &r);
8459 int x = r.x;
8460 int y = r.y;
8461
8462 if(x < 0 || x >= pTiles->m_Width)
8463 continue;
8464 if(y < 0 || y >= pTiles->m_Height)
8465 continue;
8466 CTile Tile = pTiles->GetTile(x, y);
8467 if(Tile.m_Index)
8468 m_vHoverTiles.emplace_back(
8469 args&: g, args&: l, args&: x, args&: y, args&: Tile);
8470 }
8471 }
8472 Ui()->MapScreen();
8473}
8474
8475void CEditor::HandleAutosave()
8476{
8477 const float Time = Client()->GlobalTime();
8478 const float LastAutosaveUpdateTime = m_Map.m_LastAutosaveUpdateTime;
8479 m_Map.m_LastAutosaveUpdateTime = Time;
8480
8481 if(g_Config.m_EdAutosaveInterval == 0)
8482 return; // autosave disabled
8483 if(!m_Map.m_ModifiedAuto || m_Map.m_LastModifiedTime < 0.0f)
8484 return; // no unsaved changes
8485
8486 // Add time to autosave timer if the editor was disabled for more than 10 seconds,
8487 // to prevent autosave from immediately activating when the editor is activated
8488 // after being deactivated for some time.
8489 if(LastAutosaveUpdateTime >= 0.0f && Time - LastAutosaveUpdateTime > 10.0f)
8490 {
8491 m_Map.m_LastSaveTime += Time - LastAutosaveUpdateTime;
8492 }
8493
8494 // Check if autosave timer has expired.
8495 if(m_Map.m_LastSaveTime >= Time || Time - m_Map.m_LastSaveTime < 60 * g_Config.m_EdAutosaveInterval)
8496 return;
8497
8498 // Wait for 5 seconds of no modification before saving, to prevent autosave
8499 // from immediately activating when a map is first modified or while user is
8500 // modifying the map, but don't delay the autosave for more than 1 minute.
8501 if(Time - m_Map.m_LastModifiedTime < 5.0f && Time - m_Map.m_LastSaveTime < 60 * (g_Config.m_EdAutosaveInterval + 1))
8502 return;
8503
8504 PerformAutosave();
8505}
8506
8507bool CEditor::PerformAutosave()
8508{
8509 char aDate[20];
8510 char aAutosavePath[IO_MAX_PATH_LENGTH];
8511 str_timestamp(buffer: aDate, buffer_size: sizeof(aDate));
8512 char aFileNameNoExt[IO_MAX_PATH_LENGTH];
8513 if(m_aFileName[0] == '\0')
8514 {
8515 str_copy(dst&: aFileNameNoExt, src: "unnamed");
8516 }
8517 else
8518 {
8519 const char *pFileName = fs_filename(path: m_aFileName);
8520 str_truncate(dst: aFileNameNoExt, dst_size: sizeof(aFileNameNoExt), src: pFileName, truncation_len: str_length(str: pFileName) - str_length(str: ".map"));
8521 }
8522 str_format(buffer: aAutosavePath, buffer_size: sizeof(aAutosavePath), format: "maps/auto/%s_%s.map", aFileNameNoExt, aDate);
8523
8524 m_Map.m_LastSaveTime = Client()->GlobalTime();
8525 if(Save(pFilename: aAutosavePath))
8526 {
8527 m_Map.m_ModifiedAuto = false;
8528 // Clean up autosaves
8529 if(g_Config.m_EdAutosaveMax)
8530 {
8531 CFileCollection AutosavedMaps;
8532 AutosavedMaps.Init(pStorage: Storage(), pPath: "maps/auto", pFileDesc: aFileNameNoExt, pFileExt: ".map", MaxEntries: g_Config.m_EdAutosaveMax);
8533 }
8534 return true;
8535 }
8536 else
8537 {
8538 ShowFileDialogError(pFormat: "Failed to automatically save map to file '%s'.", aAutosavePath);
8539 return false;
8540 }
8541}
8542
8543void CEditor::HandleWriterFinishJobs()
8544{
8545 if(m_WriterFinishJobs.empty())
8546 return;
8547
8548 std::shared_ptr<CDataFileWriterFinishJob> pJob = m_WriterFinishJobs.front();
8549 if(!pJob->Done())
8550 return;
8551 m_WriterFinishJobs.pop_front();
8552
8553 char aBuf[2 * IO_MAX_PATH_LENGTH + 128];
8554 if(Storage()->FileExists(pFilename: pJob->GetRealFileName(), Type: IStorage::TYPE_SAVE) && !Storage()->RemoveFile(pFilename: pJob->GetRealFileName(), Type: IStorage::TYPE_SAVE))
8555 {
8556 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Saving failed: Could not remove old map file '%s'.", pJob->GetRealFileName());
8557 ShowFileDialogError(pFormat: "%s", aBuf);
8558 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/save", pStr: aBuf);
8559 return;
8560 }
8561
8562 if(!Storage()->RenameFile(pOldFilename: pJob->GetTempFileName(), pNewFilename: pJob->GetRealFileName(), Type: IStorage::TYPE_SAVE))
8563 {
8564 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Saving failed: Could not move temporary map file '%s' to '%s'.", pJob->GetTempFileName(), pJob->GetRealFileName());
8565 ShowFileDialogError(pFormat: "%s", aBuf);
8566 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/save", pStr: aBuf);
8567 return;
8568 }
8569
8570 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "saving '%s' done", pJob->GetRealFileName());
8571 Console()->Print(Level: IConsole::OUTPUT_LEVEL_ADDINFO, pFrom: "editor/save", pStr: aBuf);
8572
8573 // send rcon.. if we can
8574 if(Client()->RconAuthed())
8575 {
8576 CServerInfo CurrentServerInfo;
8577 Client()->GetServerInfo(pServerInfo: &CurrentServerInfo);
8578 NETADDR ServerAddr = Client()->ServerAddress();
8579 const unsigned char aIpv4Localhost[16] = {127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
8580 const unsigned char aIpv6Localhost[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};
8581
8582 // and if we're on localhost
8583 if(!mem_comp(a: ServerAddr.ip, b: aIpv4Localhost, size: sizeof(aIpv4Localhost)) || !mem_comp(a: ServerAddr.ip, b: aIpv6Localhost, size: sizeof(aIpv6Localhost)))
8584 {
8585 char aMapName[128];
8586 IStorage::StripPathAndExtension(pFilename: pJob->GetRealFileName(), pBuffer: aMapName, BufferSize: sizeof(aMapName));
8587 if(!str_comp(a: aMapName, b: CurrentServerInfo.m_aMap))
8588 Client()->Rcon(pLine: "reload");
8589 }
8590 }
8591}
8592
8593void CEditor::OnUpdate()
8594{
8595 CUIElementBase::Init(pUI: Ui()); // update static pointer because game and editor use separate UI
8596
8597 if(!m_EditorWasUsedBefore)
8598 {
8599 m_EditorWasUsedBefore = true;
8600 Reset();
8601 }
8602
8603 m_pContainerPannedLast = m_pContainerPanned;
8604
8605 // handle mouse movement
8606 vec2 CursorRel = vec2(0.0f, 0.0f);
8607 IInput::ECursorType CursorType = Input()->CursorRelative(pX: &CursorRel.x, pY: &CursorRel.y);
8608 if(CursorType != IInput::CURSOR_NONE)
8609 {
8610 Ui()->ConvertMouseMove(pX: &CursorRel.x, pY: &CursorRel.y, CursorType);
8611 Ui()->OnCursorMove(X: CursorRel.x, Y: CursorRel.y);
8612 }
8613
8614 // handle key presses
8615 Input()->ConsumeEvents(Consumer: [&](const IInput::CEvent &Event) {
8616 for(CEditorComponent &Component : m_vComponents)
8617 {
8618 if(Component.OnInput(Event))
8619 return;
8620 }
8621 Ui()->OnInput(Event);
8622 });
8623
8624 HandleCursorMovement();
8625 HandleAutosave();
8626 HandleWriterFinishJobs();
8627
8628 for(CEditorComponent &Component : m_vComponents)
8629 Component.OnUpdate();
8630}
8631
8632void CEditor::OnRender()
8633{
8634 Ui()->ResetMouseSlow();
8635
8636 // toggle gui
8637 if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(Key: KEY_TAB))
8638 m_GuiActive = !m_GuiActive;
8639
8640 if(Input()->KeyPress(Key: KEY_F10))
8641 m_ShowMousePointer = false;
8642
8643 if(m_Animate)
8644 m_AnimateTime = (time_get() - m_AnimateStart) / (float)time_freq();
8645 else
8646 m_AnimateTime = 0;
8647
8648 ms_pUiGotContext = nullptr;
8649 Ui()->StartCheck();
8650
8651 Ui()->Update(MouseWorldPos: m_MouseWorldPos);
8652
8653 Render();
8654
8655 m_MouseDeltaWorld = vec2(0.0f, 0.0f);
8656
8657 if(Input()->KeyPress(Key: KEY_F10))
8658 {
8659 Graphics()->TakeScreenshot(pFilename: nullptr);
8660 m_ShowMousePointer = true;
8661 }
8662
8663 Ui()->FinishCheck();
8664 Ui()->ClearHotkeys();
8665 Input()->Clear();
8666
8667 CLineInput::RenderCandidates();
8668}
8669
8670void CEditor::OnActivate()
8671{
8672 ResetMentions();
8673 ResetIngameMoved();
8674}
8675
8676void CEditor::OnWindowResize()
8677{
8678 Ui()->OnWindowResize();
8679}
8680
8681void CEditor::OnClose()
8682{
8683 m_ColorPipetteActive = false;
8684
8685 if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_ToolbarPreviewSound))
8686 Sound()->Pause(SampleId: m_ToolbarPreviewSound);
8687 if(m_FilePreviewSound >= 0 && Sound()->IsPlaying(SampleId: m_FilePreviewSound))
8688 Sound()->Pause(SampleId: m_FilePreviewSound);
8689}
8690
8691void CEditor::OnDialogClose()
8692{
8693 Graphics()->UnloadTexture(pIndex: &m_FilePreviewImage);
8694 Sound()->UnloadSample(SampleId: m_FilePreviewSound);
8695 m_FilePreviewSound = -1;
8696 m_FilePreviewState = PREVIEW_UNLOADED;
8697}
8698
8699void CEditor::LoadCurrentMap()
8700{
8701 if(Load(pFilename: m_pClient->GetCurrentMapPath(), StorageType: IStorage::TYPE_SAVE))
8702 {
8703 m_ValidSaveFilename = true;
8704 }
8705 else
8706 {
8707 Load(pFilename: m_pClient->GetCurrentMapPath(), StorageType: IStorage::TYPE_ALL);
8708 m_ValidSaveFilename = false;
8709 }
8710
8711 CGameClient *pGameClient = (CGameClient *)Kernel()->RequestInterface<IGameClient>();
8712 vec2 Center = pGameClient->m_Camera.m_Center;
8713
8714 MapView()->SetWorldOffset(Center);
8715}
8716
8717bool CEditor::Save(const char *pFilename)
8718{
8719 // Check if file with this name is already being saved at the moment
8720 if(std::any_of(first: std::begin(cont&: m_WriterFinishJobs), last: std::end(cont&: m_WriterFinishJobs), pred: [pFilename](const std::shared_ptr<CDataFileWriterFinishJob> &Job) { return str_comp(a: pFilename, b: Job->GetRealFileName()) == 0; }))
8721 return false;
8722
8723 return m_Map.Save(pFilename);
8724}
8725
8726bool CEditor::HandleMapDrop(const char *pFileName, int StorageType)
8727{
8728 if(HasUnsavedData())
8729 {
8730 str_copy(dst&: m_aFileNamePending, src: pFileName);
8731 m_PopupEventType = CEditor::POPEVENT_LOADDROP;
8732 m_PopupEventActivated = true;
8733 return true;
8734 }
8735 else
8736 {
8737 return Load(pFilename: pFileName, StorageType: IStorage::TYPE_ALL_OR_ABSOLUTE);
8738 }
8739}
8740
8741bool CEditor::Load(const char *pFileName, int StorageType)
8742{
8743 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
8744 ShowFileDialogError(pFormat: "%s", pErrorMessage);
8745 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/load", pStr: pErrorMessage);
8746 };
8747
8748 Reset();
8749 bool Result = m_Map.Load(pFilename: pFileName, StorageType, ErrorHandler: std::move(ErrorHandler));
8750 if(Result)
8751 {
8752 str_copy(dst&: m_aFileName, src: pFileName);
8753 SortImages();
8754 SelectGameLayer();
8755
8756 for(CEditorComponent &Component : m_vComponents)
8757 Component.OnMapLoad();
8758 }
8759 else
8760 {
8761 m_aFileName[0] = 0;
8762 }
8763 return Result;
8764}
8765
8766bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory)
8767{
8768 CEditorMap NewMap;
8769 NewMap.m_pEditor = this;
8770
8771 const auto &&ErrorHandler = [this](const char *pErrorMessage) {
8772 ShowFileDialogError(pFormat: "%s", pErrorMessage);
8773 Console()->Print(Level: IConsole::OUTPUT_LEVEL_STANDARD, pFrom: "editor/append", pStr: pErrorMessage);
8774 };
8775 if(!NewMap.Load(pFilename: pFileName, StorageType, ErrorHandler: std::move(ErrorHandler)))
8776 return false;
8777
8778 CEditorActionAppendMap::SPrevInfo Info{
8779 .m_Groups: (int)m_Map.m_vpGroups.size(),
8780 .m_Images: (int)m_Map.m_vpImages.size(),
8781 .m_Sounds: (int)m_Map.m_vpSounds.size(),
8782 .m_Envelopes: (int)m_Map.m_vpEnvelopes.size()};
8783
8784 // Keep a map to check if specific indices have already been replaced to prevent
8785 // replacing those indices again when transfering images
8786 static std::map<int *, bool> s_ReplacedMap;
8787 static const auto &&s_ReplaceIndex = [](int ToReplace, int ReplaceWith) {
8788 return [ToReplace, ReplaceWith](int *pIndex) {
8789 if(*pIndex == ToReplace && !s_ReplacedMap[pIndex])
8790 {
8791 *pIndex = ReplaceWith;
8792 s_ReplacedMap[pIndex] = true;
8793 }
8794 };
8795 };
8796
8797 const auto &&Rename = [&](const std::shared_ptr<CEditorImage> &pImage) {
8798 char aRenamed[IO_MAX_PATH_LENGTH];
8799 int DuplicateCount = 1;
8800 str_copy(dst&: aRenamed, src: pImage->m_aName);
8801 while(std::find_if(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), pred: [aRenamed](const std::shared_ptr<CEditorImage> &OtherImage) { return str_comp(a: OtherImage->m_aName, b: aRenamed) == 0; }) != m_Map.m_vpImages.end())
8802 str_format(buffer: aRenamed, buffer_size: sizeof(aRenamed), format: "%s (%d)", pImage->m_aName, DuplicateCount++); // Rename to "image_name (%d)"
8803 str_copy(dst&: pImage->m_aName, src: aRenamed);
8804 };
8805
8806 // Transfer non-duplicate images
8807 s_ReplacedMap.clear();
8808 for(auto NewMapIt = NewMap.m_vpImages.begin(); NewMapIt != NewMap.m_vpImages.end(); ++NewMapIt)
8809 {
8810 auto pNewImage = *NewMapIt;
8811 auto NameIsTaken = [pNewImage](const std::shared_ptr<CEditorImage> &OtherImage) { return str_comp(a: pNewImage->m_aName, b: OtherImage->m_aName) == 0; };
8812 auto MatchInCurrentMap = std::find_if(first: m_Map.m_vpImages.begin(), last: m_Map.m_vpImages.end(), pred: NameIsTaken);
8813
8814 const bool IsDuplicate = MatchInCurrentMap != m_Map.m_vpImages.end();
8815 const int IndexToReplace = NewMapIt - NewMap.m_vpImages.begin();
8816
8817 if(IsDuplicate)
8818 {
8819 // Check for image data
8820 const bool ImageDataEquals = (*MatchInCurrentMap)->DataEquals(Other: *pNewImage);
8821
8822 if(ImageDataEquals)
8823 {
8824 const int IndexToReplaceWith = MatchInCurrentMap - m_Map.m_vpImages.begin();
8825
8826 dbg_msg(sys: "editor", fmt: "map already contains image %s with the same data, removing duplicate", pNewImage->m_aName);
8827
8828 // In the new map, replace the index of the duplicate image to the index of the same in the current map.
8829 NewMap.ModifyImageIndex(pfnFunc: s_ReplaceIndex(IndexToReplace, IndexToReplaceWith));
8830 }
8831 else
8832 {
8833 // Rename image and add it
8834 Rename(pNewImage);
8835
8836 dbg_msg(sys: "editor", fmt: "map already contains image %s but contents of appended image is different. Renaming to %s", (*MatchInCurrentMap)->m_aName, pNewImage->m_aName);
8837
8838 NewMap.ModifyImageIndex(pfnFunc: s_ReplaceIndex(IndexToReplace, m_Map.m_vpImages.size()));
8839 m_Map.m_vpImages.push_back(x: pNewImage);
8840 }
8841 }
8842 else
8843 {
8844 NewMap.ModifyImageIndex(pfnFunc: s_ReplaceIndex(IndexToReplace, m_Map.m_vpImages.size()));
8845 m_Map.m_vpImages.push_back(x: pNewImage);
8846 }
8847 }
8848 NewMap.m_vpImages.clear();
8849
8850 // modify indices
8851 static const auto &&s_ModifyAddIndex = [](int AddAmount) {
8852 return [AddAmount](int *pIndex) {
8853 if(*pIndex >= 0)
8854 *pIndex += AddAmount;
8855 };
8856 };
8857
8858 NewMap.ModifySoundIndex(pfnFunc: s_ModifyAddIndex(m_Map.m_vpSounds.size()));
8859 NewMap.ModifyEnvelopeIndex(pfnFunc: s_ModifyAddIndex(m_Map.m_vpEnvelopes.size()));
8860
8861 // transfer sounds
8862 for(const auto &pSound : NewMap.m_vpSounds)
8863 m_Map.m_vpSounds.push_back(x: pSound);
8864 NewMap.m_vpSounds.clear();
8865
8866 // transfer envelopes
8867 for(const auto &pEnvelope : NewMap.m_vpEnvelopes)
8868 m_Map.m_vpEnvelopes.push_back(x: pEnvelope);
8869 NewMap.m_vpEnvelopes.clear();
8870
8871 // transfer groups
8872 for(const auto &pGroup : NewMap.m_vpGroups)
8873 {
8874 if(pGroup != NewMap.m_pGameGroup)
8875 {
8876 pGroup->m_pMap = &m_Map;
8877 m_Map.m_vpGroups.push_back(x: pGroup);
8878 }
8879 }
8880 NewMap.m_vpGroups.clear();
8881
8882 auto IndexMap = SortImages();
8883
8884 if(!IgnoreHistory)
8885 m_EditorHistory.RecordAction(pAction: std::make_shared<CEditorActionAppendMap>(args: this, args&: pFileName, args&: Info, args&: IndexMap));
8886
8887 // all done \o/
8888 return true;
8889}
8890
8891void CEditor::UndoLastAction()
8892{
8893 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
8894 m_ServerSettingsHistory.Undo();
8895 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
8896 m_EnvelopeEditorHistory.Undo();
8897 else
8898 m_EditorHistory.Undo();
8899}
8900
8901void CEditor::RedoLastAction()
8902{
8903 if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS)
8904 m_ServerSettingsHistory.Redo();
8905 else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES)
8906 m_EnvelopeEditorHistory.Redo();
8907 else
8908 m_EditorHistory.Redo();
8909}
8910
8911void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int Adjust)
8912{
8913 // Adjust m_Number field of tune, switch and tele tiles by `Adjust` if `UseNextFree` is false
8914 // If true, then use the next free number instead
8915
8916 auto &&AdjustNumber = [Adjust](unsigned char &Number) {
8917 Number = ((Number + Adjust) - 1 + 255) % 255 + 1;
8918 };
8919
8920 for(auto &pLayer : m_pBrush->m_vpLayers)
8921 {
8922 if(pLayer->m_Type != LAYERTYPE_TILES)
8923 continue;
8924
8925 std::shared_ptr<CLayerTiles> pLayerTiles = std::static_pointer_cast<CLayerTiles>(r: pLayer);
8926
8927 // Only handle tele, switch and tune layers
8928 if(pLayerTiles->m_Tele)
8929 {
8930 int NextFreeTeleNumber = FindNextFreeTeleNumber();
8931 int NextFreeCPNumber = FindNextFreeTeleNumber(IsCheckpoint: true);
8932
8933 std::shared_ptr<CLayerTele> pTeleLayer = std::static_pointer_cast<CLayerTele>(r: pLayer);
8934 for(int y = 0; y < pTeleLayer->m_Height; y++)
8935 {
8936 for(int x = 0; x < pTeleLayer->m_Width; x++)
8937 {
8938 int i = y * pTeleLayer->m_Width + x;
8939 if(!IsValidTeleTile(Index: pTeleLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTeleLayer->m_pTeleTile[i].m_Number))
8940 continue;
8941
8942 if(UseNextFree)
8943 {
8944 if(IsTeleTileCheckpoint(Index: pTeleLayer->m_pTiles[i].m_Index))
8945 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeCPNumber;
8946 else
8947 pTeleLayer->m_pTeleTile[i].m_Number = NextFreeTeleNumber;
8948 }
8949 else
8950 AdjustNumber(pTeleLayer->m_pTeleTile[i].m_Number);
8951 }
8952 }
8953 }
8954 else if(pLayerTiles->m_Tune)
8955 {
8956 if(!UseNextFree)
8957 {
8958 std::shared_ptr<CLayerTune> pTuneLayer = std::static_pointer_cast<CLayerTune>(r: pLayer);
8959 for(int y = 0; y < pTuneLayer->m_Height; y++)
8960 {
8961 for(int x = 0; x < pTuneLayer->m_Width; x++)
8962 {
8963 int i = y * pTuneLayer->m_Width + x;
8964 if(!IsValidTuneTile(Index: pTuneLayer->m_pTiles[i].m_Index) || !pTuneLayer->m_pTuneTile[i].m_Number)
8965 continue;
8966
8967 AdjustNumber(pTuneLayer->m_pTuneTile[i].m_Number);
8968 }
8969 }
8970 }
8971 }
8972 else if(pLayerTiles->m_Switch)
8973 {
8974 int NextFreeNumber = FindNextFreeSwitchNumber();
8975
8976 std::shared_ptr<CLayerSwitch> pSwitchLayer = std::static_pointer_cast<CLayerSwitch>(r: pLayer);
8977 for(int y = 0; y < pSwitchLayer->m_Height; y++)
8978 {
8979 for(int x = 0; x < pSwitchLayer->m_Width; x++)
8980 {
8981 int i = y * pSwitchLayer->m_Width + x;
8982 if(!IsValidSwitchTile(Index: pSwitchLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pSwitchLayer->m_pSwitchTile[i].m_Number))
8983 continue;
8984
8985 if(UseNextFree)
8986 pSwitchLayer->m_pSwitchTile[i].m_Number = NextFreeNumber;
8987 else
8988 AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Number);
8989 }
8990 }
8991 }
8992 }
8993}
8994
8995int CEditor::FindNextFreeSwitchNumber()
8996{
8997 int Number = -1;
8998
8999 for(int i = 1; i <= 255; i++)
9000 {
9001 if(!m_Map.m_pSwitchLayer->ContainsElementWithId(Id: i))
9002 {
9003 Number = i;
9004 break;
9005 }
9006 }
9007 return Number;
9008}
9009
9010int CEditor::FindNextFreeTeleNumber(bool IsCheckpoint)
9011{
9012 int Number = -1;
9013 for(int i = 1; i <= 255; i++)
9014 {
9015 if(!m_Map.m_pTeleLayer->ContainsElementWithId(Id: i, Checkpoint: IsCheckpoint))
9016 {
9017 Number = i;
9018 break;
9019 }
9020 }
9021 return Number;
9022}
9023
9024IEditor *CreateEditor() { return new CEditor; }
9025