Line data Source code
1 : /* Copyright (C) 2022 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "GUIManager.h"
21 :
22 : #include "gui/CGUI.h"
23 : #include "lib/timer.h"
24 : #include "ps/CLogger.h"
25 : #include "ps/Filesystem.h"
26 : #include "ps/GameSetup/Config.h"
27 : #include "ps/Profile.h"
28 : #include "ps/VideoMode.h"
29 : #include "ps/XML/Xeromyces.h"
30 : #include "scriptinterface/FunctionWrapper.h"
31 : #include "scriptinterface/ScriptContext.h"
32 : #include "scriptinterface/ScriptInterface.h"
33 : #include "scriptinterface/StructuredClone.h"
34 :
35 : namespace
36 : {
37 :
38 1 : const CStr EVENT_NAME_GAME_LOAD_PROGRESS = "GameLoadProgress";
39 1 : const CStr EVENT_NAME_WINDOW_RESIZED = "WindowResized";
40 :
41 : } // anonymous namespace
42 :
43 : CGUIManager* g_GUI = nullptr;
44 :
45 : // General TODOs:
46 : //
47 : // A lot of the CGUI data could (and should) be shared between
48 : // multiple pages, instead of treating them as completely independent, to save
49 : // memory and loading time.
50 :
51 :
52 : // called from main loop when (input) events are received.
53 : // event is passed to other handlers if false is returned.
54 : // trampoline: we don't want to make the HandleEvent implementation static
55 7 : InReaction gui_handler(const SDL_Event_* ev)
56 : {
57 7 : if (!g_GUI)
58 0 : return IN_PASS;
59 :
60 14 : PROFILE("GUI event handler");
61 7 : return g_GUI->HandleEvent(ev);
62 : }
63 :
64 0 : static Status ReloadChangedFileCB(void* param, const VfsPath& path)
65 : {
66 0 : return static_cast<CGUIManager*>(param)->ReloadChangedFile(path);
67 : }
68 :
69 3 : CGUIManager::CGUIManager()
70 : {
71 3 : m_ScriptContext = g_ScriptContext;
72 3 : m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIManager", m_ScriptContext));
73 3 : m_ScriptInterface->SetCallbackData(this);
74 3 : m_ScriptInterface->LoadGlobalScripts();
75 :
76 3 : if (!CXeromyces::AddValidator(g_VFS, "gui_page", "gui/gui_page.rng"))
77 0 : LOGERROR("CGUIManager: failed to load GUI page grammar file 'gui/gui_page.rng'");
78 3 : if (!CXeromyces::AddValidator(g_VFS, "gui", "gui/gui.rng"))
79 0 : LOGERROR("CGUIManager: failed to load GUI XML grammar file 'gui/gui.rng'");
80 :
81 3 : RegisterFileReloadFunc(ReloadChangedFileCB, this);
82 3 : }
83 :
84 6 : CGUIManager::~CGUIManager()
85 : {
86 3 : UnregisterFileReloadFunc(ReloadChangedFileCB, this);
87 3 : }
88 :
89 1 : size_t CGUIManager::GetPageCount() const
90 : {
91 1 : return m_PageStack.size();
92 : }
93 :
94 0 : void CGUIManager::SwitchPage(const CStrW& pageName, const ScriptInterface* srcScriptInterface, JS::HandleValue initData)
95 : {
96 : // The page stack is cleared (including the script context where initData came from),
97 : // therefore we have to clone initData.
98 :
99 0 : Script::StructuredClone initDataClone;
100 0 : if (!initData.isUndefined())
101 : {
102 0 : ScriptRequest rq(srcScriptInterface);
103 0 : initDataClone = Script::WriteStructuredClone(rq, initData);
104 : }
105 :
106 0 : if (!m_PageStack.empty())
107 : {
108 : // Make sure we unfocus anything on the current page.
109 0 : m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
110 0 : m_PageStack.clear();
111 : }
112 :
113 0 : PushPage(pageName, initDataClone, JS::UndefinedHandleValue);
114 0 : }
115 :
116 6 : void CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone initData, JS::HandleValue callbackFunction)
117 : {
118 : // Store the callback handler in the current GUI page before opening the new one
119 6 : if (!m_PageStack.empty() && !callbackFunction.isUndefined())
120 : {
121 1 : m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction);
122 :
123 : // Make sure we unfocus anything on the current page.
124 1 : m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
125 : }
126 :
127 : // Push the page prior to loading its contents, because that may push
128 : // another GUI page on init which should be pushed on top of this new page.
129 6 : m_PageStack.emplace_back(pageName, initData);
130 6 : m_PageStack.back().LoadPage(m_ScriptContext);
131 6 : }
132 :
133 3 : void CGUIManager::PopPage(Script::StructuredClone args)
134 : {
135 3 : if (m_PageStack.size() < 2)
136 : {
137 0 : debug_warn(L"Tried to pop GUI page when there's < 2 in the stack");
138 0 : return;
139 : }
140 :
141 : // Make sure we unfocus anything on the current page.
142 3 : m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
143 :
144 3 : m_PageStack.pop_back();
145 3 : m_PageStack.back().PerformCallbackFunction(args);
146 :
147 : // We return to a page where some object might have been focused.
148 3 : m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS);
149 : }
150 :
151 6 : CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const Script::StructuredClone initData)
152 6 : : m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction()
153 : {
154 6 : }
155 :
156 6 : void CGUIManager::SGUIPage::LoadPage(std::shared_ptr<ScriptContext> scriptContext)
157 : {
158 : // If we're hotloading then try to grab some data from the previous page
159 12 : Script::StructuredClone hotloadData;
160 6 : if (gui)
161 : {
162 0 : std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
163 0 : ScriptRequest rq(scriptInterface);
164 :
165 0 : JS::RootedValue global(rq.cx, rq.globalValue());
166 0 : JS::RootedValue hotloadDataVal(rq.cx);
167 0 : ScriptFunction::Call(rq, global, "getHotloadData", &hotloadDataVal);
168 0 : hotloadData = Script::WriteStructuredClone(rq, hotloadDataVal);
169 : }
170 :
171 6 : g_VideoMode.ResetCursor();
172 6 : inputs.clear();
173 6 : gui.reset(new CGUI(scriptContext));
174 :
175 6 : gui->AddObjectTypes();
176 :
177 12 : VfsPath path = VfsPath("gui") / m_Name;
178 6 : inputs.insert(path);
179 :
180 12 : CXeromyces xero;
181 6 : if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK)
182 : // Fail silently (Xeromyces reported the error)
183 0 : return;
184 :
185 6 : int elmt_page = xero.GetElementID("page");
186 6 : int elmt_include = xero.GetElementID("include");
187 :
188 6 : XMBElement root = xero.GetRoot();
189 :
190 6 : if (root.GetNodeName() != elmt_page)
191 : {
192 0 : LOGERROR("GUI page '%s' must have root element <page>", utf8_from_wstring(m_Name));
193 0 : return;
194 : }
195 :
196 14 : XERO_ITER_EL(root, node)
197 : {
198 8 : if (node.GetNodeName() != elmt_include)
199 : {
200 0 : LOGERROR("GUI page '%s' must only have <include> elements inside <page>", utf8_from_wstring(m_Name));
201 0 : continue;
202 : }
203 :
204 16 : CStr8 name = node.GetText();
205 16 : CStrW nameW = node.GetText().FromUTF8();
206 :
207 16 : PROFILE2("load gui xml");
208 8 : PROFILE2_ATTR("name: %s", name.c_str());
209 :
210 16 : TIMER(nameW.c_str());
211 8 : if (name.back() == '/')
212 : {
213 0 : VfsPath currentDirectory = VfsPath("gui") / nameW;
214 0 : VfsPaths directories;
215 0 : vfs::GetPathnames(g_VFS, currentDirectory, L"*.xml", directories);
216 0 : for (const VfsPath& directory : directories)
217 0 : gui->LoadXmlFile(directory, inputs);
218 : }
219 : else
220 : {
221 16 : VfsPath directory = VfsPath("gui") / nameW;
222 8 : gui->LoadXmlFile(directory, inputs);
223 : }
224 : }
225 :
226 6 : gui->LoadedXmlFiles();
227 :
228 12 : std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
229 12 : ScriptRequest rq(scriptInterface);
230 :
231 12 : JS::RootedValue initDataVal(rq.cx);
232 12 : JS::RootedValue hotloadDataVal(rq.cx);
233 12 : JS::RootedValue global(rq.cx, rq.globalValue());
234 :
235 6 : if (initData)
236 6 : Script::ReadStructuredClone(rq, initData, &initDataVal);
237 :
238 6 : if (hotloadData)
239 0 : Script::ReadStructuredClone(rq, hotloadData, &hotloadDataVal);
240 :
241 12 : if (Script::HasProperty(rq, global, "init") &&
242 6 : !ScriptFunction::CallVoid(rq, global, "init", initDataVal, hotloadDataVal))
243 0 : LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name));
244 : }
245 :
246 1 : void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc)
247 : {
248 1 : if (!callbackFunc.isObject())
249 : {
250 0 : LOGERROR("Given callback handler is not an object!");
251 0 : return;
252 : }
253 :
254 2 : ScriptRequest rq(scriptInterface);
255 :
256 1 : if (!JS_ObjectIsFunction(&callbackFunc.toObject()))
257 : {
258 0 : LOGERROR("Given callback handler is not a function!");
259 0 : return;
260 : }
261 :
262 1 : callbackFunction = std::make_shared<JS::PersistentRootedValue>(scriptInterface.GetGeneralJSContext(), callbackFunc);
263 : }
264 :
265 3 : void CGUIManager::SGUIPage::PerformCallbackFunction(Script::StructuredClone args)
266 : {
267 3 : if (!callbackFunction)
268 2 : return;
269 :
270 2 : std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
271 2 : ScriptRequest rq(scriptInterface);
272 :
273 2 : JS::RootedObject globalObj(rq.cx, rq.glob);
274 :
275 2 : JS::RootedValue funcVal(rq.cx, *callbackFunction);
276 :
277 : // Delete the callback function, so that it is not called again
278 1 : callbackFunction.reset();
279 :
280 2 : JS::RootedValue argVal(rq.cx);
281 1 : if (args)
282 1 : Script::ReadStructuredClone(rq, args, &argVal);
283 :
284 2 : JS::RootedValueVector paramData(rq.cx);
285 1 : ignore_result(paramData.append(argVal));
286 :
287 2 : JS::RootedValue result(rq.cx);
288 :
289 1 : if(!JS_CallFunctionValue(rq.cx, globalObj, funcVal, paramData, &result))
290 0 : ScriptException::CatchPending(rq);
291 : }
292 :
293 0 : Status CGUIManager::ReloadChangedFile(const VfsPath& path)
294 : {
295 0 : for (SGUIPage& p : m_PageStack)
296 0 : if (p.inputs.find(path) != p.inputs.end())
297 : {
298 0 : LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.m_Name));
299 0 : p.LoadPage(m_ScriptContext);
300 : // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
301 : }
302 :
303 0 : return INFO::OK;
304 : }
305 :
306 0 : Status CGUIManager::ReloadAllPages()
307 : {
308 : // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators
309 0 : for (SGUIPage& p : m_PageStack)
310 0 : p.LoadPage(m_ScriptContext);
311 :
312 0 : return INFO::OK;
313 : }
314 :
315 7 : InReaction CGUIManager::HandleEvent(const SDL_Event_* ev)
316 : {
317 : // We want scripts to have access to the raw input events, so they can do complex
318 : // processing when necessary (e.g. for unit selection and camera movement).
319 : // Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the
320 : // visible game area), sometimes they'll want to intercepts events before the GUI (e.g.
321 : // to capture all mouse events until a mouseup after dragging).
322 : // So we call two separate handler functions:
323 :
324 7 : bool handled = false;
325 :
326 : {
327 14 : PROFILE("handleInputBeforeGui");
328 14 : ScriptRequest rq(*top()->GetScriptInterface());
329 :
330 14 : JS::RootedValue global(rq.cx, rq.globalValue());
331 7 : if (ScriptFunction::Call(rq, global, "handleInputBeforeGui", handled, *ev, top()->FindObjectUnderMouse()))
332 7 : if (handled)
333 0 : return IN_HANDLED;
334 : }
335 :
336 : {
337 14 : PROFILE("handle event in native GUI");
338 7 : InReaction r = top()->HandleEvent(ev);
339 7 : if (r != IN_PASS)
340 0 : return r;
341 : }
342 :
343 : {
344 : // We can't take the following lines out of this scope because top() may be another gui page than it was when calling handleInputBeforeGui!
345 14 : ScriptRequest rq(*top()->GetScriptInterface());
346 14 : JS::RootedValue global(rq.cx, rq.globalValue());
347 :
348 14 : PROFILE("handleInputAfterGui");
349 7 : if (ScriptFunction::Call(rq, global, "handleInputAfterGui", handled, *ev))
350 7 : if (handled)
351 0 : return IN_HANDLED;
352 : }
353 :
354 7 : return IN_PASS;
355 : }
356 :
357 0 : void CGUIManager::SendEventToAll(const CStr& eventName) const
358 : {
359 : // Save an immutable copy so iterators aren't invalidated by handlers
360 0 : PageStackType pageStack = m_PageStack;
361 :
362 0 : for (const SGUIPage& p : pageStack)
363 0 : p.gui->SendEventToAll(eventName);
364 :
365 0 : }
366 :
367 0 : void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const
368 : {
369 : // Save an immutable copy so iterators aren't invalidated by handlers
370 0 : PageStackType pageStack = m_PageStack;
371 :
372 0 : for (const SGUIPage& p : pageStack)
373 0 : p.gui->SendEventToAll(eventName, paramData);
374 0 : }
375 :
376 2 : void CGUIManager::TickObjects()
377 : {
378 4 : PROFILE3("gui tick");
379 :
380 : // We share the script context with everything else that runs in the same thread.
381 : // This call makes sure we trigger GC regularly even if the simulation is not running.
382 2 : m_ScriptInterface->GetContext()->MaybeIncrementalGC(1.0f);
383 :
384 : // Save an immutable copy so iterators aren't invalidated by tick handlers
385 4 : PageStackType pageStack = m_PageStack;
386 :
387 4 : for (const SGUIPage& p : pageStack)
388 2 : p.gui->TickObjects();
389 2 : }
390 :
391 0 : void CGUIManager::Draw(CCanvas2D& canvas) const
392 : {
393 0 : PROFILE3_GPU("gui");
394 :
395 0 : for (const SGUIPage& p : m_PageStack)
396 0 : p.gui->Draw(canvas);
397 0 : }
398 :
399 0 : void CGUIManager::UpdateResolution()
400 : {
401 : // Save an immutable copy so iterators aren't invalidated by event handlers
402 0 : PageStackType pageStack = m_PageStack;
403 :
404 0 : for (const SGUIPage& p : pageStack)
405 : {
406 0 : p.gui->UpdateResolution();
407 0 : p.gui->SendEventToAll(EVENT_NAME_WINDOW_RESIZED);
408 : }
409 0 : }
410 :
411 0 : bool CGUIManager::TemplateExists(const std::string& templateName) const
412 : {
413 0 : return m_TemplateLoader.TemplateExists(templateName);
414 : }
415 :
416 0 : const CParamNode& CGUIManager::GetTemplate(const std::string& templateName)
417 : {
418 0 : const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetOnlyChild();
419 0 : if (!templateRoot.IsOk())
420 0 : LOGERROR("Invalid template found for '%s'", templateName.c_str());
421 :
422 0 : return templateRoot;
423 : }
424 :
425 0 : void CGUIManager::DisplayLoadProgress(int percent, const wchar_t* pending_task)
426 : {
427 0 : const ScriptInterface& scriptInterface = *(GetActiveGUI()->GetScriptInterface());
428 0 : ScriptRequest rq(scriptInterface);
429 :
430 0 : JS::RootedValueVector paramData(rq.cx);
431 :
432 0 : ignore_result(paramData.append(JS::NumberValue(percent)));
433 :
434 0 : JS::RootedValue valPendingTask(rq.cx);
435 0 : Script::ToJSVal(rq, &valPendingTask, pending_task);
436 0 : ignore_result(paramData.append(valPendingTask));
437 :
438 0 : SendEventToAll(EVENT_NAME_GAME_LOAD_PROGRESS, paramData);
439 0 : }
440 :
441 : // This returns a shared_ptr to make sure the CGUI doesn't get deallocated
442 : // while we're in the middle of calling a function on it (e.g. if a GUI script
443 : // calls SwitchPage)
444 31 : std::shared_ptr<CGUI> CGUIManager::top() const
445 : {
446 31 : ENSURE(m_PageStack.size());
447 31 : return m_PageStack.back().gui;
448 3 : }
|