1#include "editor_history.h"
2
3#include "editor.h"
4#include "editor_actions.h"
5
6#include <engine/font_icons.h>
7#include <engine/shared/config.h>
8
9void CEditorHistory::RecordAction(const std::shared_ptr<IEditorAction> &pAction)
10{
11 RecordAction(pAction, pDisplay: nullptr);
12}
13
14void CEditorHistory::Execute(const std::shared_ptr<IEditorAction> &pAction, const char *pDisplay)
15{
16 pAction->Redo();
17 RecordAction(pAction, pDisplay);
18}
19
20void CEditorHistory::RecordAction(const std::shared_ptr<IEditorAction> &pAction, const char *pDisplay)
21{
22 if(m_IsBulk)
23 {
24 m_vpBulkActions.push_back(x: pAction);
25 return;
26 }
27
28 m_vpRedoActions.clear();
29
30 if((int)m_vpUndoActions.size() >= g_Config.m_ClEditorMaxHistory)
31 {
32 m_vpUndoActions.pop_front();
33 }
34
35 if(pDisplay == nullptr)
36 m_vpUndoActions.emplace_back(args: pAction);
37 else
38 m_vpUndoActions.emplace_back(args: std::make_shared<CEditorActionBulk>(args: Map(), args: std::vector<std::shared_ptr<IEditorAction>>{pAction}, args&: pDisplay));
39}
40
41bool CEditorHistory::Undo()
42{
43 if(m_vpUndoActions.empty())
44 return false;
45
46 auto pLastAction = m_vpUndoActions.back();
47 m_vpUndoActions.pop_back();
48
49 pLastAction->Undo();
50
51 m_vpRedoActions.emplace_back(args&: pLastAction);
52 return true;
53}
54
55bool CEditorHistory::Redo()
56{
57 if(m_vpRedoActions.empty())
58 return false;
59
60 auto pLastAction = m_vpRedoActions.back();
61 m_vpRedoActions.pop_back();
62
63 pLastAction->Redo();
64
65 m_vpUndoActions.emplace_back(args&: pLastAction);
66 return true;
67}
68
69void CEditorHistory::Clear()
70{
71 m_vpUndoActions.clear();
72 m_vpRedoActions.clear();
73}
74
75void CEditorHistory::BeginBulk()
76{
77 m_IsBulk = true;
78 m_vpBulkActions.clear();
79}
80
81void CEditorHistory::EndBulk(const char *pDisplay)
82{
83 if(!m_IsBulk)
84 return;
85 m_IsBulk = false;
86
87 // Record bulk action
88 if(!m_vpBulkActions.empty())
89 RecordAction(pAction: std::make_shared<CEditorActionBulk>(args: Map(), args&: m_vpBulkActions, args&: pDisplay, args: true));
90
91 m_vpBulkActions.clear();
92}
93
94void CEditorHistory::EndBulk(int DisplayToUse)
95{
96 EndBulk(pDisplay: (DisplayToUse < 0 || DisplayToUse >= (int)m_vpBulkActions.size()) ? nullptr : m_vpBulkActions[DisplayToUse]->DisplayText());
97}
98
99void CEditor::RenderEditorHistory(CUIRect View)
100{
101 CEditorHistoryUiState &State = Map()->m_EditorHistoryUiState;
102 CListBox &ListBox = State.m_aListBoxes[(int)State.m_HistoryType];
103 int &SelectedActionIndex = State.m_aSelectedActionIndices[(int)State.m_HistoryType];
104
105 ListBox.SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen());
106
107 const bool GotSelection = ListBox.Active() && SelectedActionIndex >= 0 && (size_t)SelectedActionIndex < Map()->m_vSettings.size();
108
109 CUIRect ToolBar, Button, Label, List, DragBar;
110 View.HSplitTop(Cut: 22.0f, pTop: &DragBar, pBottom: nullptr);
111 DragBar.y -= 2.0f;
112 DragBar.w += 2.0f;
113 DragBar.h += 4.0f;
114 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_HISTORY]);
115 View.HSplitTop(Cut: 20.0f, pTop: &ToolBar, pBottom: &View);
116 View.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &List);
117 ToolBar.HMargin(Cut: 2.0f, pOtherRect: &ToolBar);
118
119 CUIRect TypeButtons, HistoryTypeButton;
120 const int HistoryTypeBtnSize = 70.0f;
121 ToolBar.VSplitLeft(Cut: 3 * HistoryTypeBtnSize, pLeft: &TypeButtons, pRight: &Label);
122
123 // history type buttons
124 {
125 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
126 if(DoButton_Ex(pId: &State.m_aHistoryTypeButtonIds[(int)EHistoryType::EDITOR], pText: "Editor", Checked: State.m_HistoryType == EHistoryType::EDITOR, pRect: &HistoryTypeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Show map editor history.", Corners: IGraphics::CORNER_L))
127 {
128 State.m_HistoryType = EHistoryType::EDITOR;
129 }
130
131 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
132 if(DoButton_Ex(pId: &State.m_aHistoryTypeButtonIds[(int)EHistoryType::ENVELOPE], pText: "Envelope", Checked: State.m_HistoryType == EHistoryType::ENVELOPE, pRect: &HistoryTypeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Show envelope editor history.", Corners: IGraphics::CORNER_NONE))
133 {
134 State.m_HistoryType = EHistoryType::ENVELOPE;
135 }
136
137 TypeButtons.VSplitLeft(Cut: HistoryTypeBtnSize, pLeft: &HistoryTypeButton, pRight: &TypeButtons);
138 if(DoButton_Ex(pId: &State.m_aHistoryTypeButtonIds[(int)EHistoryType::SERVER_SETTINGS], pText: "Settings", Checked: State.m_HistoryType == EHistoryType::SERVER_SETTINGS, pRect: &HistoryTypeButton, Flags: BUTTONFLAG_LEFT, pToolTip: "Show server settings editor history.", Corners: IGraphics::CORNER_R))
139 {
140 State.m_HistoryType = EHistoryType::SERVER_SETTINGS;
141 }
142 }
143
144 SLabelProperties InfoProps;
145 InfoProps.m_MaxWidth = ToolBar.w - 60.f;
146 InfoProps.m_EllipsisAtEnd = true;
147 Label.VSplitLeft(Cut: 8.0f, pLeft: nullptr, pRight: &Label);
148 Ui()->DoLabel(pRect: &Label, pText: "Editor history. Click on an action to undo all actions above.", Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: InfoProps);
149
150 CEditorHistory *pCurrentHistory;
151 if(State.m_HistoryType == EHistoryType::EDITOR)
152 {
153 pCurrentHistory = &Map()->m_EditorHistory;
154 }
155 else if(State.m_HistoryType == EHistoryType::ENVELOPE)
156 {
157 pCurrentHistory = &Map()->m_EnvelopeEditorHistory;
158 }
159 else if(State.m_HistoryType == EHistoryType::SERVER_SETTINGS)
160 {
161 pCurrentHistory = &Map()->m_ServerSettingsHistory;
162 }
163 else
164 {
165 dbg_assert_failed("Invalid State.m_HistoryType: %d", (int)State.m_HistoryType);
166 }
167
168 // delete button
169 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
170 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
171 if(DoButton_FontIcon(pId: &State.m_DeleteButtonId, pText: FontIcon::TRASH, Checked: (!pCurrentHistory->m_vpUndoActions.empty() || !pCurrentHistory->m_vpRedoActions.empty()) ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "Clear the history.", Corners: IGraphics::CORNER_ALL, FontSize: 9.0f) ||
172 (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
173 {
174 pCurrentHistory->Clear();
175 SelectedActionIndex = 0;
176 }
177
178 // actions list
179 int RedoSize = (int)pCurrentHistory->m_vpRedoActions.size();
180 int UndoSize = (int)pCurrentHistory->m_vpUndoActions.size();
181 SelectedActionIndex = RedoSize;
182 ListBox.DoStart(RowHeight: 15.0f, NumItems: RedoSize + UndoSize, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: SelectedActionIndex, pRect: &List);
183
184 for(int i = 0; i < RedoSize; i++)
185 {
186 const CListboxItem Item = ListBox.DoNextItem(pId: &pCurrentHistory->m_vpRedoActions[i], Selected: SelectedActionIndex >= 0 && SelectedActionIndex == i);
187 if(!Item.m_Visible)
188 continue;
189
190 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
191
192 SLabelProperties Props;
193 Props.m_MaxWidth = Label.w;
194 Props.m_EllipsisAtEnd = true;
195 TextRender()->TextColor(Color: {.5f, .5f, .5f});
196 TextRender()->TextOutlineColor(Color: TextRender()->DefaultTextOutlineColor());
197 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpRedoActions[i]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
198 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
199 }
200
201 for(int i = 0; i < UndoSize; i++)
202 {
203 const CListboxItem Item = ListBox.DoNextItem(pId: &pCurrentHistory->m_vpUndoActions[UndoSize - i - 1], Selected: SelectedActionIndex >= RedoSize && SelectedActionIndex == (i + RedoSize));
204 if(!Item.m_Visible)
205 continue;
206
207 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
208
209 SLabelProperties Props;
210 Props.m_MaxWidth = Label.w;
211 Props.m_EllipsisAtEnd = true;
212 Ui()->DoLabel(pRect: &Label, pText: pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
213 }
214
215 { // Base action "Loaded map" that cannot be undone
216 const CListboxItem Item = ListBox.DoNextItem(pId: &State.m_BaseActionButtonId, Selected: SelectedActionIndex == RedoSize + UndoSize);
217 if(Item.m_Visible)
218 {
219 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
220
221 Ui()->DoLabel(pRect: &Label, pText: "Loaded map", Size: 10.0f, Align: TEXTALIGN_ML);
222 }
223 }
224
225 const int NewSelected = ListBox.DoEnd();
226 if(SelectedActionIndex != NewSelected)
227 {
228 // Figure out if we should undo or redo some actions
229 // Undo everything until the selected index
230 if(NewSelected > SelectedActionIndex)
231 {
232 for(int i = 0; i < (NewSelected - SelectedActionIndex); i++)
233 {
234 pCurrentHistory->Undo();
235 }
236 }
237 else
238 {
239 for(int i = 0; i < (SelectedActionIndex - NewSelected); i++)
240 {
241 pCurrentHistory->Redo();
242 }
243 }
244 SelectedActionIndex = NewSelected;
245 }
246}
247