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 "CGUI.h"
21 :
22 : #include "graphics/Canvas2D.h"
23 : #include "gui/IGUIScrollBar.h"
24 : #include "gui/ObjectBases/IGUIObject.h"
25 : #include "gui/ObjectTypes/CGUIDummyObject.h"
26 : #include "gui/ObjectTypes/CTooltip.h"
27 : #include "gui/Scripting/ScriptFunctions.h"
28 : #include "gui/Scripting/JSInterface_GUIProxy.h"
29 : #include "i18n/L10n.h"
30 : #include "lib/allocators/DynamicArena.h"
31 : #include "lib/allocators/STLAllocators.h"
32 : #include "lib/bits.h"
33 : #include "lib/input.h"
34 : #include "lib/sysdep/sysdep.h"
35 : #include "lib/timer.h"
36 : #include "lib/utf8.h"
37 : #include "maths/Size2D.h"
38 : #include "ps/CLogger.h"
39 : #include "ps/Filesystem.h"
40 : #include "ps/GameSetup/Config.h"
41 : #include "ps/Globals.h"
42 : #include "ps/Hotkey.h"
43 : #include "ps/Profile.h"
44 : #include "ps/Pyrogenesis.h"
45 : #include "ps/VideoMode.h"
46 : #include "ps/XML/Xeromyces.h"
47 : #include "scriptinterface/ScriptContext.h"
48 : #include "scriptinterface/ScriptInterface.h"
49 :
50 : #include <string>
51 : #include <unordered_map>
52 : #include <unordered_set>
53 :
54 : const double SELECT_DBLCLICK_RATE = 0.5;
55 : const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion
56 :
57 1 : const CStr CGUI::EventNameLoad = "Load";
58 1 : const CStr CGUI::EventNameTick = "Tick";
59 1 : const CStr CGUI::EventNamePress = "Press";
60 1 : const CStr CGUI::EventNameKeyDown = "KeyDown";
61 1 : const CStr CGUI::EventNameRelease = "Release";
62 1 : const CStr CGUI::EventNameMouseRightPress = "MouseRightPress";
63 1 : const CStr CGUI::EventNameMouseLeftPress = "MouseLeftPress";
64 1 : const CStr CGUI::EventNameMouseWheelDown = "MouseWheelDown";
65 1 : const CStr CGUI::EventNameMouseWheelUp = "MouseWheelUp";
66 1 : const CStr CGUI::EventNameMouseLeftDoubleClick = "MouseLeftDoubleClick";
67 1 : const CStr CGUI::EventNameMouseLeftRelease = "MouseLeftRelease";
68 1 : const CStr CGUI::EventNameMouseRightDoubleClick = "MouseRightDoubleClick";
69 1 : const CStr CGUI::EventNameMouseRightRelease = "MouseRightRelease";
70 :
71 : namespace
72 : {
73 :
74 : struct VisibleObject
75 : {
76 : IGUIObject* object;
77 : // Index of the object in a depth-first search inside GUI tree.
78 : u32 index;
79 : // Cached value of GetBufferedZ to avoid recursive calls in a deep hierarchy.
80 : float bufferedZ;
81 : };
82 :
83 : template<class Container>
84 0 : void CollectVisibleObjectsRecursively(const std::vector<IGUIObject*>& objects, Container* visibleObjects)
85 : {
86 0 : for (IGUIObject* const& object : objects)
87 : {
88 0 : if (!object->IsHidden())
89 : {
90 0 : visibleObjects->emplace_back(VisibleObject{object, static_cast<u32>(visibleObjects->size()), 0.0f});
91 0 : CollectVisibleObjectsRecursively(object->GetChildren(), visibleObjects);
92 : }
93 : }
94 0 : }
95 :
96 : } // anonynous namespace
97 :
98 12 : CGUI::CGUI(const std::shared_ptr<ScriptContext>& context)
99 24 : : m_BaseObject(std::make_unique<CGUIDummyObject>(*this)),
100 : m_FocusedObject(nullptr),
101 : m_InternalNameNumber(0),
102 36 : m_MouseButtons(0)
103 : {
104 12 : m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "GUIPage", context);
105 12 : m_ScriptInterface->SetCallbackData(this);
106 :
107 12 : GuiScriptingInit(*m_ScriptInterface);
108 12 : m_ScriptInterface->LoadGlobalScripts();
109 12 : }
110 :
111 24 : CGUI::~CGUI()
112 : {
113 16 : for (const std::pair<const CStr, IGUIObject*>& p : m_pAllObjects)
114 4 : delete p.second;
115 12 : }
116 :
117 7 : InReaction CGUI::HandleEvent(const SDL_Event_* ev)
118 : {
119 7 : InReaction ret = IN_PASS;
120 :
121 7 : if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYUP)
122 : {
123 4 : const char* hotkey = static_cast<const char*>(ev->ev.user.data1);
124 :
125 4 : const CStr& eventName = ev->ev.type == SDL_HOTKEYPRESS ? EventNamePress : ev->ev.type == SDL_HOTKEYDOWN ? EventNameKeyDown : EventNameRelease;
126 :
127 4 : if (m_GlobalHotkeys.find(hotkey) != m_GlobalHotkeys.end() && m_GlobalHotkeys[hotkey].find(eventName) != m_GlobalHotkeys[hotkey].end())
128 : {
129 0 : ret = IN_HANDLED;
130 :
131 0 : ScriptRequest rq(m_ScriptInterface);
132 0 : JS::RootedObject globalObj(rq.cx, rq.glob);
133 0 : JS::RootedValue result(rq.cx);
134 0 : if (!JS_CallFunctionValue(rq.cx, globalObj, m_GlobalHotkeys[hotkey][eventName], JS::HandleValueArray::empty(), &result))
135 0 : ScriptException::CatchPending(rq);
136 : }
137 :
138 4 : std::map<CStr, std::vector<IGUIObject*> >::iterator it = m_HotkeyObjects.find(hotkey);
139 4 : if (it != m_HotkeyObjects.end())
140 0 : for (IGUIObject* const& obj : it->second)
141 : {
142 0 : if (!obj->IsEnabled())
143 0 : continue;
144 0 : if (ev->ev.type == SDL_HOTKEYPRESS)
145 0 : ret = obj->SendEvent(GUIM_PRESSED, EventNamePress);
146 0 : else if (ev->ev.type == SDL_HOTKEYDOWN)
147 0 : ret = obj->SendEvent(GUIM_KEYDOWN, EventNameKeyDown);
148 : else
149 0 : ret = obj->SendEvent(GUIM_RELEASED, EventNameRelease);
150 4 : }
151 : }
152 :
153 3 : else if (ev->ev.type == SDL_MOUSEMOTION)
154 : {
155 : // Yes the mouse position is stored as float to avoid
156 : // constant conversions when operating in a
157 : // float-based environment.
158 0 : m_MousePos = CVector2D((float)ev->ev.motion.x / g_VideoMode.GetScale(), (float)ev->ev.motion.y / g_VideoMode.GetScale());
159 :
160 0 : SGUIMessage msg(GUIM_MOUSE_MOTION);
161 0 : m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::HandleMessage, msg);
162 : }
163 :
164 : // Update m_MouseButtons. (BUTTONUP is handled later.)
165 3 : else if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
166 : {
167 0 : switch (ev->ev.button.button)
168 : {
169 0 : case SDL_BUTTON_LEFT:
170 : case SDL_BUTTON_RIGHT:
171 : case SDL_BUTTON_MIDDLE:
172 0 : m_MouseButtons |= Bit<unsigned int>(ev->ev.button.button);
173 0 : break;
174 0 : default:
175 0 : break;
176 : }
177 : }
178 :
179 : // Update m_MousePos (for delayed mouse button events)
180 7 : CVector2D oldMousePos = m_MousePos;
181 7 : if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
182 : {
183 0 : m_MousePos = CVector2D((float)ev->ev.button.x / g_VideoMode.GetScale(), (float)ev->ev.button.y / g_VideoMode.GetScale());
184 : }
185 :
186 : // Allow the focused object to pre-empt regular GUI events.
187 7 : if (GetFocusedObject())
188 0 : ret = GetFocusedObject()->PreemptEvent(ev);
189 :
190 : // Only one object can be hovered
191 : // pNearest will after this point at the hovered object, possibly nullptr
192 7 : IGUIObject* pNearest = FindObjectUnderMouse();
193 :
194 7 : if (ret == IN_PASS)
195 : {
196 : // Now we'll call UpdateMouseOver on *all* objects,
197 : // we'll input the one hovered, and they will each
198 : // update their own data and send messages accordingly
199 7 : m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
200 :
201 7 : if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
202 : {
203 0 : switch (ev->ev.button.button)
204 : {
205 0 : case SDL_BUTTON_LEFT:
206 : // Focus the clicked object (or focus none if nothing clicked on)
207 0 : SetFocusedObject(pNearest);
208 :
209 0 : if (pNearest)
210 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT, EventNameMouseLeftPress);
211 0 : break;
212 :
213 0 : case SDL_BUTTON_RIGHT:
214 0 : if (pNearest)
215 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT, EventNameMouseRightPress);
216 0 : break;
217 :
218 0 : default:
219 0 : break;
220 : }
221 : }
222 7 : else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest)
223 : {
224 0 : if (ev->ev.wheel.y < 0)
225 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_DOWN, EventNameMouseWheelDown);
226 0 : else if (ev->ev.wheel.y > 0)
227 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_UP, EventNameMouseWheelUp);
228 : }
229 7 : else if (ev->ev.type == SDL_MOUSEBUTTONUP)
230 : {
231 0 : switch (ev->ev.button.button)
232 : {
233 0 : case SDL_BUTTON_LEFT:
234 0 : if (pNearest)
235 : {
236 0 : double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT];
237 0 : pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time();
238 0 : if (timeElapsed < SELECT_DBLCLICK_RATE)
239 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT, EventNameMouseLeftDoubleClick);
240 : else
241 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_LEFT, EventNameMouseLeftRelease);
242 : }
243 0 : break;
244 0 : case SDL_BUTTON_RIGHT:
245 0 : if (pNearest)
246 : {
247 0 : double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT];
248 0 : pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time();
249 0 : if (timeElapsed < SELECT_DBLCLICK_RATE)
250 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_RIGHT, EventNameMouseRightDoubleClick);
251 : else
252 0 : ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_RIGHT, EventNameMouseRightRelease);
253 : }
254 0 : break;
255 : }
256 :
257 : // Reset all states on all visible objects
258 0 : m_BaseObject->RecurseObject(&IGUIObject::IsHidden, &IGUIObject::ResetStates);
259 :
260 : // Since the hover state will have been reset, we reload it.
261 0 : m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
262 : }
263 : }
264 :
265 : // BUTTONUP's effect on m_MouseButtons is handled after
266 : // everything else, so that e.g. 'press' handlers (activated
267 : // on button up) see which mouse button had been pressed.
268 7 : if (ev->ev.type == SDL_MOUSEBUTTONUP)
269 : {
270 0 : switch (ev->ev.button.button)
271 : {
272 0 : case SDL_BUTTON_LEFT:
273 : case SDL_BUTTON_RIGHT:
274 : case SDL_BUTTON_MIDDLE:
275 0 : m_MouseButtons &= ~Bit<unsigned int>(ev->ev.button.button);
276 0 : break;
277 0 : default:
278 0 : break;
279 : }
280 : }
281 :
282 : // Restore m_MousePos (for delayed mouse button events)
283 7 : if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
284 0 : m_MousePos = oldMousePos;
285 :
286 : // Let GUI items handle keys after everything else, e.g. for input boxes.
287 7 : if (ret == IN_PASS && GetFocusedObject())
288 : {
289 0 : if (ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_KEYDOWN ||
290 0 : ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYDOWN ||
291 0 : ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING)
292 0 : ret = GetFocusedObject()->ManuallyHandleKeys(ev);
293 : // else will return IN_PASS because we never used the button.
294 : }
295 :
296 7 : return ret;
297 : }
298 :
299 2 : void CGUI::TickObjects()
300 : {
301 2 : m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::Tick);
302 2 : SendEventToAll(EventNameTick);
303 2 : m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
304 2 : }
305 :
306 8 : void CGUI::SendEventToAll(const CStr& eventName)
307 : {
308 8 : std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it = m_EventObjects.find(eventName);
309 8 : if (it == m_EventObjects.end())
310 6 : return;
311 :
312 4 : std::vector<IGUIObject*> copy = it->second;
313 7 : for (IGUIObject* object : copy)
314 5 : object->ScriptEvent(eventName);
315 : }
316 :
317 0 : void CGUI::SendEventToAll(const CStr& eventName, const JS::HandleValueArray& paramData)
318 : {
319 0 : std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it = m_EventObjects.find(eventName);
320 0 : if (it == m_EventObjects.end())
321 0 : return;
322 :
323 0 : std::vector<IGUIObject*> copy = it->second;
324 0 : for (IGUIObject* object : copy)
325 0 : object->ScriptEvent(eventName, paramData);
326 : }
327 :
328 0 : void CGUI::Draw(CCanvas2D& canvas)
329 : {
330 : using Arena = Allocators::DynamicArena<128 * KiB>;
331 : using ObjectListAllocator = ProxyAllocator<VisibleObject, Arena>;
332 0 : Arena arena;
333 :
334 0 : std::vector<VisibleObject, ObjectListAllocator> visibleObjects((ObjectListAllocator(arena)));
335 0 : CollectVisibleObjectsRecursively(m_BaseObject->GetChildren(), &visibleObjects);
336 0 : for (VisibleObject& visibleObject : visibleObjects)
337 0 : visibleObject.bufferedZ = visibleObject.object->GetBufferedZ();
338 :
339 0 : std::sort(visibleObjects.begin(), visibleObjects.end(), [](const VisibleObject& visibleObject1, const VisibleObject& visibleObject2) -> bool {
340 0 : if (visibleObject1.bufferedZ != visibleObject2.bufferedZ)
341 0 : return visibleObject1.bufferedZ < visibleObject2.bufferedZ;
342 0 : return visibleObject1.index < visibleObject2.index;
343 : });
344 :
345 0 : for (const VisibleObject& visibleObject : visibleObjects)
346 0 : visibleObject.object->Draw(canvas);
347 0 : }
348 :
349 0 : void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, CCanvas2D& canvas, const CRect& Rect, const CRect& UNUSED(Clipping))
350 : {
351 : // If the sprite doesn't exist (name == ""), don't bother drawing anything
352 0 : if (!Sprite)
353 0 : return;
354 :
355 : // TODO: Clipping?
356 :
357 0 : Sprite.Draw(*this, canvas, Rect, m_Sprites);
358 : }
359 :
360 0 : void CGUI::UpdateResolution()
361 : {
362 0 : m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
363 0 : }
364 :
365 4 : IGUIObject* CGUI::ConstructObject(const CStr& str)
366 : {
367 4 : std::map<CStr, ConstructObjectFunction>::iterator it = m_ObjectTypes.find(str);
368 :
369 4 : if (it == m_ObjectTypes.end())
370 0 : return nullptr;
371 :
372 4 : return (*it->second)(*this);
373 : }
374 :
375 4 : bool CGUI::AddObject(IGUIObject& parent, IGUIObject& child)
376 : {
377 4 : if (child.m_Name.empty())
378 : {
379 0 : LOGERROR("Can't register an object without name!");
380 0 : return false;
381 : }
382 :
383 4 : if (m_pAllObjects.find(child.m_Name) != m_pAllObjects.end())
384 : {
385 0 : LOGERROR("Can't register more than one object of the name %s", child.m_Name.c_str());
386 0 : return false;
387 : }
388 :
389 4 : m_pAllObjects[child.m_Name] = &child;
390 4 : parent.RegisterChild(&child);
391 4 : return true;
392 : }
393 :
394 61 : IGUIObject* CGUI::GetBaseObject()
395 : {
396 61 : return m_BaseObject.get();
397 : };
398 :
399 0 : bool CGUI::ObjectExists(const CStr& Name) const
400 : {
401 0 : return m_pAllObjects.find(Name) != m_pAllObjects.end();
402 : }
403 :
404 5 : IGUIObject* CGUI::FindObjectByName(const CStr& Name) const
405 : {
406 5 : map_pObjects::const_iterator it = m_pAllObjects.find(Name);
407 :
408 5 : if (it == m_pAllObjects.end())
409 0 : return nullptr;
410 :
411 5 : return it->second;
412 : }
413 :
414 16 : IGUIObject* CGUI::FindObjectUnderMouse()
415 : {
416 16 : IGUIObject* pNearest = nullptr;
417 16 : m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::ChooseMouseOverAndClosest, pNearest);
418 16 : return pNearest;
419 : }
420 :
421 4 : CSize2D CGUI::GetWindowSize() const
422 : {
423 4 : return CSize2D{static_cast<float>(g_xres) / g_VideoMode.GetScale(), static_cast<float>(g_yres) / g_VideoMode.GetScale() };
424 : }
425 :
426 7 : void CGUI::SendFocusMessage(EGUIMessageType msgType)
427 : {
428 7 : if (m_FocusedObject)
429 : {
430 0 : SGUIMessage msg(msgType);
431 0 : m_FocusedObject->HandleMessage(msg);
432 : }
433 7 : }
434 :
435 0 : void CGUI::SetFocusedObject(IGUIObject* pObject)
436 : {
437 0 : if (pObject == m_FocusedObject)
438 0 : return;
439 :
440 0 : if (m_FocusedObject)
441 : {
442 0 : SGUIMessage msg(GUIM_LOST_FOCUS);
443 0 : m_FocusedObject->HandleMessage(msg);
444 : }
445 :
446 0 : m_FocusedObject = pObject;
447 :
448 0 : if (m_FocusedObject)
449 : {
450 0 : SGUIMessage msg(GUIM_GOT_FOCUS);
451 0 : m_FocusedObject->HandleMessage(msg);
452 : }
453 : }
454 :
455 4 : void CGUI::SetObjectStyle(IGUIObject* pObject, const CStr& styleName)
456 : {
457 : // If the style is not recognised (or an empty string) then ApplyStyle will
458 : // emit an error message. Thus we don't need to handle it here.
459 4 : pObject->ApplyStyle(styleName);
460 4 : }
461 :
462 0 : void CGUI::UnsetObjectStyle(IGUIObject* pObject)
463 : {
464 0 : SetObjectStyle(pObject, "default");
465 0 : }
466 :
467 0 : void CGUI::SetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag)
468 : {
469 0 : if (!hotkeyTag.empty())
470 0 : m_HotkeyObjects[hotkeyTag].push_back(pObject);
471 0 : }
472 :
473 0 : void CGUI::UnsetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag)
474 : {
475 0 : if (hotkeyTag.empty())
476 0 : return;
477 :
478 0 : std::vector<IGUIObject*>& assignment = m_HotkeyObjects[hotkeyTag];
479 :
480 0 : assignment.erase(
481 0 : std::remove_if(
482 : assignment.begin(),
483 : assignment.end(),
484 0 : [&pObject](const IGUIObject* hotkeyObject)
485 0 : { return pObject == hotkeyObject; }),
486 0 : assignment.end());
487 : }
488 :
489 0 : void CGUI::SetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName, JS::HandleValue function)
490 : {
491 0 : ScriptRequest rq(*m_ScriptInterface);
492 :
493 0 : if (hotkeyTag.empty())
494 : {
495 0 : ScriptException::Raise(rq, "Cannot assign a function to an empty hotkey identifier!");
496 0 : return;
497 : }
498 :
499 : // Only support "Press", "Keydown" and "Release" events.
500 0 : if (eventName != EventNamePress && eventName != EventNameKeyDown && eventName != EventNameRelease)
501 : {
502 0 : ScriptException::Raise(rq, "Cannot assign a function to an unsupported event!");
503 0 : return;
504 : }
505 :
506 0 : if (!function.isObject() || !JS_ObjectIsFunction(&function.toObject()))
507 : {
508 0 : ScriptException::Raise(rq, "Cannot assign non-function value to global hotkey '%s'", hotkeyTag.c_str());
509 0 : return;
510 : }
511 :
512 0 : UnsetGlobalHotkey(hotkeyTag, eventName);
513 0 : m_GlobalHotkeys[hotkeyTag][eventName].init(rq.cx, function);
514 : }
515 :
516 0 : void CGUI::UnsetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName)
517 : {
518 0 : std::map<CStr, std::map<CStr, JS::PersistentRootedValue>>::iterator it = m_GlobalHotkeys.find(hotkeyTag);
519 0 : if (it == m_GlobalHotkeys.end())
520 0 : return;
521 :
522 0 : m_GlobalHotkeys[hotkeyTag].erase(eventName);
523 :
524 0 : if (m_GlobalHotkeys.count(hotkeyTag) == 0)
525 0 : m_GlobalHotkeys.erase(it);
526 : }
527 :
528 0 : const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const
529 : {
530 0 : std::map<CStr, const SGUIScrollBarStyle>::const_iterator it = m_ScrollBarStyles.find(style);
531 0 : if (it == m_ScrollBarStyles.end())
532 0 : return nullptr;
533 :
534 0 : return &it->second;
535 : }
536 :
537 : /**
538 : * @callgraph
539 : */
540 8 : void CGUI::LoadXmlFile(const VfsPath& Filename, std::unordered_set<VfsPath>& Paths)
541 : {
542 8 : Paths.insert(Filename);
543 :
544 16 : CXeromyces xeroFile;
545 8 : if (xeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK)
546 : // The error has already been reported by CXeromyces
547 0 : return;
548 :
549 8 : XMBElement node = xeroFile.GetRoot();
550 8 : std::string_view root_name(xeroFile.GetElementStringView(node.GetNodeName()));
551 :
552 8 : if (root_name == "objects")
553 3 : Xeromyces_ReadRootObjects(xeroFile, node, Paths);
554 5 : else if (root_name == "sprites")
555 0 : Xeromyces_ReadRootSprites(xeroFile, node);
556 5 : else if (root_name == "styles")
557 5 : Xeromyces_ReadRootStyles(xeroFile, node);
558 0 : else if (root_name == "setup")
559 0 : Xeromyces_ReadRootSetup(xeroFile, node);
560 : else
561 0 : LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name.data());
562 : }
563 :
564 6 : void CGUI::LoadedXmlFiles()
565 : {
566 6 : m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
567 :
568 12 : SGUIMessage msg(GUIM_LOAD);
569 6 : m_BaseObject->RecurseObject(nullptr, &IGUIObject::HandleMessage, msg);
570 :
571 6 : SendEventToAll(EventNameLoad);
572 6 : }
573 :
574 : //===================================================================
575 : // XML Reading Xeromyces Specific Sub-Routines
576 : //===================================================================
577 :
578 3 : void CGUI::Xeromyces_ReadRootObjects(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
579 : {
580 3 : int el_script = xmb.GetElementID("script");
581 :
582 6 : std::vector<std::pair<CStr, CStr> > subst;
583 :
584 : // Iterate main children
585 : // they should all be <object> or <script> elements
586 10 : for (XMBElement child : element.GetChildNodes())
587 : {
588 7 : if (child.GetNodeName() == el_script)
589 : // Execute the inline script
590 3 : Xeromyces_ReadScript(xmb, child, Paths);
591 : else
592 : // Read in this whole object into the GUI
593 4 : Xeromyces_ReadObject(xmb, child, m_BaseObject.get(), subst, Paths, 0);
594 : }
595 3 : }
596 :
597 0 : void CGUI::Xeromyces_ReadRootSprites(const XMBData& xmb, XMBElement element)
598 : {
599 0 : for (XMBElement child : element.GetChildNodes())
600 0 : Xeromyces_ReadSprite(xmb, child);
601 0 : }
602 :
603 5 : void CGUI::Xeromyces_ReadRootStyles(const XMBData& xmb, XMBElement element)
604 : {
605 10 : for (XMBElement child : element.GetChildNodes())
606 5 : Xeromyces_ReadStyle(xmb, child);
607 5 : }
608 :
609 0 : void CGUI::Xeromyces_ReadRootSetup(const XMBData& xmb, XMBElement element)
610 : {
611 0 : for (XMBElement child : element.GetChildNodes())
612 : {
613 0 : std::string_view name(xmb.GetElementStringView(child.GetNodeName()));
614 0 : if (name == "scrollbar")
615 0 : Xeromyces_ReadScrollBarStyle(xmb, child);
616 0 : else if (name == "icon")
617 0 : Xeromyces_ReadIcon(xmb, child);
618 0 : else if (name == "tooltip")
619 0 : Xeromyces_ReadTooltip(xmb, child);
620 0 : else if (name == "color")
621 0 : Xeromyces_ReadColor(xmb, child);
622 : else
623 0 : debug_warn(L"Invalid data - DTD shouldn't allow this");
624 : }
625 0 : }
626 :
627 4 : IGUIObject* CGUI::Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObject* pParent, std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth)
628 : {
629 4 : ENSURE(pParent);
630 :
631 4 : XMBAttributeList attributes = element.GetAttributes();
632 :
633 8 : CStr type(attributes.GetNamedItem(xmb.GetAttributeID("type")));
634 4 : if (type.empty())
635 4 : type = "empty";
636 :
637 : // Construct object from specified type
638 : // henceforth, we need to do a rollback before aborting.
639 : // i.e. releasing this object
640 4 : IGUIObject* object = ConstructObject(type);
641 :
642 4 : if (!object)
643 : {
644 0 : LOGERROR("GUI: Unrecognized object type \"%s\"", type.c_str());
645 0 : return nullptr;
646 : }
647 :
648 : // Cache some IDs for element attribute names, to avoid string comparisons
649 : #define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
650 : #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
651 4 : ELMT(object);
652 4 : ELMT(action);
653 4 : ELMT(script);
654 4 : ELMT(repeat);
655 4 : ELMT(translatableAttribute);
656 4 : ELMT(translate);
657 4 : ELMT(attribute);
658 4 : ELMT(keep);
659 4 : ELMT(include);
660 4 : ATTR(style);
661 4 : ATTR(type);
662 4 : ATTR(name);
663 4 : ATTR(z);
664 4 : ATTR(on);
665 4 : ATTR(file);
666 4 : ATTR(directory);
667 4 : ATTR(id);
668 4 : ATTR(context);
669 :
670 : //
671 : // Read Style and set defaults
672 : //
673 : // If the setting "style" is set, try loading that setting.
674 : //
675 : // Always load default (if it's available) first!
676 : //
677 4 : SetObjectStyle(object, "default");
678 :
679 8 : CStr argStyle(attributes.GetNamedItem(attr_style));
680 4 : if (!argStyle.empty())
681 0 : SetObjectStyle(object, argStyle);
682 :
683 4 : bool NameSet = false;
684 4 : bool ManuallySetZ = false;
685 :
686 4 : for (XMBAttribute attr : attributes)
687 : {
688 : // If value is "null", then it is equivalent as never being entered
689 4 : if (attr.Value == "null")
690 0 : continue;
691 :
692 : // Ignore "type" and "style", we've already checked it
693 4 : if (attr.Name == attr_type || attr.Name == attr_style)
694 0 : continue;
695 :
696 4 : if (attr.Name == attr_name)
697 : {
698 8 : CStr name(attr.Value);
699 :
700 4 : if (name.Left(2) == "__")
701 : {
702 0 : LOGERROR("GUI: Names starting with '__' are reserved for the engine (object: %s)", name.c_str());
703 0 : continue;
704 : }
705 :
706 4 : for (const std::pair<CStr, CStr>& sub : NameSubst)
707 0 : name.Replace(sub.first, sub.second);
708 :
709 4 : object->SetName(name);
710 4 : NameSet = true;
711 4 : continue;
712 : }
713 :
714 0 : if (attr.Name == attr_z)
715 0 : ManuallySetZ = true;
716 :
717 0 : object->SetSettingFromString(xmb.GetAttributeString(attr.Name), attr.Value.FromUTF8(), false);
718 : }
719 :
720 : // Check if name isn't set, generate an internal name in that case.
721 4 : if (!NameSet)
722 : {
723 0 : object->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber) + ")");
724 0 : ++m_InternalNameNumber;
725 : }
726 :
727 8 : CStrW caption(element.GetText().FromUTF8());
728 4 : if (!caption.empty())
729 0 : object->SetSettingFromString("caption", caption, false);
730 :
731 4 : for (XMBElement child : element.GetChildNodes())
732 : {
733 : // Check what name the elements got
734 0 : int element_name = child.GetNodeName();
735 :
736 0 : if (element_name == elmt_object)
737 : {
738 : // Call this function on the child
739 0 : Xeromyces_ReadObject(xmb, child, object, NameSubst, Paths, nesting_depth);
740 : }
741 0 : else if (element_name == elmt_action)
742 : {
743 : // Scripted <action> element
744 :
745 : // Check for a 'file' parameter
746 0 : CStrW filename(child.GetAttributes().GetNamedItem(attr_file).FromUTF8());
747 :
748 0 : CStr code;
749 :
750 : // If there is a file, open it and use it as the code
751 0 : if (!filename.empty())
752 : {
753 0 : Paths.insert(filename);
754 0 : CVFSFile scriptfile;
755 0 : if (scriptfile.Load(g_VFS, filename) != PSRETURN_OK)
756 : {
757 0 : LOGERROR("Error opening GUI script action file '%s'", utf8_from_wstring(filename));
758 0 : continue;
759 : }
760 :
761 0 : code = scriptfile.DecodeUTF8(); // assume it's UTF-8
762 : }
763 :
764 0 : XMBElementList grandchildren = child.GetChildNodes();
765 0 : if (!grandchildren.empty()) // The <action> element contains <keep> and <translate> tags.
766 0 : for (XMBElement grandchild : grandchildren)
767 : {
768 0 : if (grandchild.GetNodeName() == elmt_translate)
769 0 : code += g_L10n.Translate(grandchild.GetText());
770 0 : else if (grandchild.GetNodeName() == elmt_keep)
771 0 : code += grandchild.GetText();
772 : }
773 : else // It's pure JavaScript code.
774 : // Read the inline code (concatenating to the file code, if both are specified)
775 0 : code += CStr(child.GetText());
776 :
777 0 : CStr eventName = child.GetAttributes().GetNamedItem(attr_on);
778 0 : object->RegisterScriptHandler(eventName, code, *this);
779 : }
780 0 : else if (child.GetNodeName() == elmt_script)
781 : {
782 0 : Xeromyces_ReadScript(xmb, child, Paths);
783 : }
784 0 : else if (element_name == elmt_repeat)
785 : {
786 0 : Xeromyces_ReadRepeat(xmb, child, object, NameSubst, Paths, nesting_depth);
787 : }
788 0 : else if (element_name == elmt_translatableAttribute)
789 : {
790 : // This is an element in the form "<translatableAttribute id="attributeName">attributeValue</translatableAttribute>".
791 0 : CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name.
792 0 : if (attributeName.empty())
793 : {
794 0 : LOGERROR("GUI: 'translatableAttribute' XML element with empty 'id' XML attribute found. (object: %s)", object->GetPresentableName().c_str());
795 0 : continue;
796 : }
797 :
798 0 : CStr value(child.GetText());
799 0 : if (value.empty())
800 0 : continue;
801 :
802 0 : CStr context(child.GetAttributes().GetNamedItem(attr_context)); // Read the context if any.
803 :
804 0 : CStr translatedValue = context.empty() ?
805 0 : g_L10n.Translate(value) :
806 0 : g_L10n.TranslateWithContext(context, value);
807 :
808 0 : object->SetSettingFromString(attributeName, translatedValue.FromUTF8(), false);
809 : }
810 0 : else if (element_name == elmt_attribute)
811 : {
812 : // This is an element in the form "<attribute id="attributeName"><keep>Don't translate this part
813 : // </keep><translate>but translate this one.</translate></attribute>".
814 0 : CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name.
815 0 : if (attributeName.empty())
816 : {
817 0 : LOGERROR("GUI: 'attribute' XML element with empty 'id' XML attribute found. (object: %s)", object->GetPresentableName().c_str());
818 0 : continue;
819 : }
820 :
821 0 : CStr translatedValue;
822 :
823 0 : for (XMBElement grandchild : child.GetChildNodes())
824 : {
825 0 : if (grandchild.GetNodeName() == elmt_translate)
826 0 : translatedValue += g_L10n.Translate(grandchild.GetText());
827 0 : else if (grandchild.GetNodeName() == elmt_keep)
828 0 : translatedValue += grandchild.GetText();
829 : }
830 0 : object->SetSettingFromString(attributeName, translatedValue.FromUTF8(), false);
831 : }
832 0 : else if (element_name == elmt_include)
833 : {
834 0 : CStrW filename(child.GetAttributes().GetNamedItem(attr_file).FromUTF8());
835 0 : CStrW directory(child.GetAttributes().GetNamedItem(attr_directory).FromUTF8());
836 0 : if (!filename.empty())
837 : {
838 0 : if (!directory.empty())
839 0 : LOGWARNING("GUI: Include element found with file name (%s) and directory name (%s). Only the file will be processed.", utf8_from_wstring(filename), utf8_from_wstring(directory));
840 :
841 0 : Paths.insert(filename);
842 :
843 0 : CXeromyces xeroIncluded;
844 0 : if (xeroIncluded.Load(g_VFS, filename, "gui") != PSRETURN_OK)
845 : {
846 0 : LOGERROR("GUI: Error reading included XML: '%s'", utf8_from_wstring(filename));
847 0 : continue;
848 : }
849 :
850 0 : XMBElement node = xeroIncluded.GetRoot();
851 0 : if (node.GetNodeName() != xeroIncluded.GetElementID("object"))
852 : {
853 0 : LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", utf8_from_wstring(filename));
854 0 : continue;
855 : }
856 :
857 0 : if (nesting_depth+1 >= MAX_OBJECT_DEPTH)
858 : {
859 0 : LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(filename));
860 0 : continue;
861 : }
862 :
863 0 : Xeromyces_ReadObject(xeroIncluded, node, object, NameSubst, Paths, nesting_depth+1);
864 : }
865 0 : else if (!directory.empty())
866 : {
867 0 : if (nesting_depth+1 >= MAX_OBJECT_DEPTH)
868 : {
869 0 : LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(directory));
870 0 : continue;
871 : }
872 :
873 0 : VfsPaths pathnames;
874 0 : vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames);
875 0 : for (const VfsPath& path : pathnames)
876 : {
877 : // as opposed to loading scripts, don't care if it's loaded before
878 : // one might use the same parts of the GUI in different situations
879 0 : Paths.insert(path);
880 0 : CXeromyces xeroIncluded;
881 0 : if (xeroIncluded.Load(g_VFS, path, "gui") != PSRETURN_OK)
882 : {
883 0 : LOGERROR("GUI: Error reading included XML: '%s'", path.string8());
884 0 : continue;
885 : }
886 :
887 0 : XMBElement node = xeroIncluded.GetRoot();
888 0 : if (node.GetNodeName() != xeroIncluded.GetElementID("object"))
889 : {
890 0 : LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", path.string8());
891 0 : continue;
892 : }
893 0 : Xeromyces_ReadObject(xeroIncluded, node, object, NameSubst, Paths, nesting_depth+1);
894 : }
895 :
896 : }
897 : else
898 0 : LOGERROR("GUI: 'include' XML element must have valid 'file' or 'directory' attribute found. (object %s)", object->GetPresentableName().c_str());
899 : }
900 : else
901 : {
902 : // Try making the object read the tag.
903 0 : if (!object->HandleAdditionalChildren(xmb, child))
904 0 : LOGERROR("GUI: (object: %s) Reading unknown children for its type", object->GetPresentableName().c_str());
905 : }
906 : }
907 :
908 4 : object->AdditionalChildrenHandled();
909 :
910 4 : if (!ManuallySetZ)
911 : {
912 : // Set it automatically to 10 plus its parents
913 4 : if (object->m_Absolute)
914 : // If the object is absolute, we'll have to get the parent's Z buffered,
915 : // and add to that!
916 4 : object->m_Z.Set(pParent->GetBufferedZ() + 10.f, false);
917 : else
918 : // If the object is relative, then we'll just store Z as "10"
919 0 : object->m_Z.Set(10.f, false);
920 : }
921 :
922 4 : if (!AddObject(*pParent, *object))
923 : {
924 0 : delete object;
925 0 : return nullptr;
926 : }
927 4 : return object;
928 : }
929 :
930 0 : void CGUI::Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObject* pParent, std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth)
931 : {
932 : #define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
933 : #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
934 0 : ELMT(object);
935 0 : ATTR(count);
936 0 : ATTR(var);
937 :
938 0 : XMBAttributeList attributes = element.GetAttributes();
939 :
940 0 : int count = CStr(attributes.GetNamedItem(attr_count)).ToInt();
941 0 : CStr var("["+attributes.GetNamedItem(attr_var)+"]");
942 0 : if (var.size() < 3)
943 0 : var = "[n]";
944 :
945 0 : for (int n = 0; n < count; ++n)
946 : {
947 0 : NameSubst.emplace_back(var, "[" + CStr::FromInt(n) + "]");
948 :
949 0 : XERO_ITER_EL(element, child)
950 : {
951 0 : if (child.GetNodeName() == elmt_object)
952 0 : Xeromyces_ReadObject(xmb, child, pParent, NameSubst, Paths, nesting_depth);
953 : }
954 0 : NameSubst.pop_back();
955 : }
956 0 : }
957 :
958 3 : void CGUI::Xeromyces_ReadScript(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
959 : {
960 : // Check for a 'file' parameter
961 6 : CStrW fileAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("file")).FromUTF8());
962 :
963 : // If there is a file specified, open and execute it
964 3 : if (!fileAttr.empty())
965 : {
966 3 : if (!VfsPath(fileAttr).IsDirectory())
967 : {
968 3 : Paths.insert(fileAttr);
969 3 : m_ScriptInterface->LoadGlobalScriptFile(fileAttr);
970 : }
971 : else
972 0 : LOGERROR("GUI: Script path %s is not a file path", fileAttr.ToUTF8().c_str());
973 : }
974 :
975 : // If it has a directory attribute, read all JS files in that directory
976 6 : CStrW directoryAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("directory")).FromUTF8());
977 3 : if (!directoryAttr.empty())
978 : {
979 0 : if (VfsPath(directoryAttr).IsDirectory())
980 : {
981 0 : VfsPaths pathnames;
982 0 : vfs::GetPathnames(g_VFS, directoryAttr, L"*.js", pathnames);
983 0 : for (const VfsPath& path : pathnames)
984 : {
985 : // Only load new files (so when the insert succeeds)
986 0 : if (Paths.insert(path).second)
987 0 : m_ScriptInterface->LoadGlobalScriptFile(path);
988 : }
989 : }
990 : else
991 0 : LOGERROR("GUI: Script path %s is not a directory path", directoryAttr.ToUTF8().c_str());
992 : }
993 :
994 6 : CStr code(element.GetText());
995 3 : if (!code.empty())
996 0 : m_ScriptInterface->LoadGlobalScript(L"Some XML file", code);
997 3 : }
998 :
999 0 : void CGUI::Xeromyces_ReadSprite(const XMBData& xmb, XMBElement element)
1000 : {
1001 0 : auto sprite = std::make_unique<CGUISprite>();
1002 :
1003 : // Get name, we know it exists because of DTD requirements
1004 0 : CStr name = element.GetAttributes().GetNamedItem(xmb.GetAttributeID("name"));
1005 :
1006 0 : if (m_Sprites.find(name) != m_Sprites.end())
1007 0 : LOGWARNING("GUI sprite name '%s' used more than once; first definition will be discarded", name.c_str());
1008 :
1009 : // shared_ptr to link the effect to every image, faster than copy.
1010 0 : std::shared_ptr<SGUIImageEffects> effects;
1011 :
1012 0 : for (XMBElement child : element.GetChildNodes())
1013 : {
1014 0 : std::string_view ElementName(xmb.GetElementStringView(child.GetNodeName()));
1015 0 : if (ElementName == "image")
1016 0 : Xeromyces_ReadImage(xmb, child, *sprite);
1017 0 : else if (ElementName == "effect")
1018 : {
1019 0 : if (effects)
1020 0 : LOGERROR("GUI <sprite> must not have more than one <effect>");
1021 : else
1022 : {
1023 0 : effects = std::make_shared<SGUIImageEffects>();
1024 0 : Xeromyces_ReadEffects(xmb, child, *effects);
1025 : }
1026 : }
1027 : else
1028 0 : debug_warn(L"Invalid data - DTD shouldn't allow this");
1029 : }
1030 :
1031 : // Apply the effects to every image (unless the image overrides it with
1032 : // different effects)
1033 0 : if (effects)
1034 : {
1035 0 : for (const std::unique_ptr<SGUIImage>& image : sprite->m_Images)
1036 0 : if (!image->m_Effects)
1037 0 : image->m_Effects = effects;
1038 : }
1039 :
1040 0 : m_Sprites.erase(name);
1041 0 : m_Sprites.emplace(name, std::move(sprite));
1042 0 : }
1043 :
1044 0 : void CGUI::Xeromyces_ReadImage(const XMBData& xmb, XMBElement element, CGUISprite& parent)
1045 : {
1046 0 : auto image = std::make_unique<SGUIImage>();
1047 :
1048 : // TODO Gee: Setup defaults here (or maybe they are in the SGUIImage ctor)
1049 :
1050 0 : for (XMBAttribute attr : element.GetAttributes())
1051 : {
1052 0 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1053 0 : CStrW attr_value(attr.Value.FromUTF8());
1054 :
1055 0 : if (attr_name == "texture")
1056 : {
1057 0 : image->m_TextureName = VfsPath("art/textures/ui") / attr_value;
1058 : }
1059 0 : else if (attr_name == "size")
1060 : {
1061 0 : image->m_Size.FromString(attr.Value);
1062 : }
1063 0 : else if (attr_name == "texture_size")
1064 : {
1065 0 : image->m_TextureSize.FromString(attr.Value);
1066 : }
1067 0 : else if (attr_name == "real_texture_placement")
1068 : {
1069 0 : CRect rect;
1070 0 : if (!ParseString<CRect>(this, attr_value, rect))
1071 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1072 : else
1073 0 : image->m_TexturePlacementInFile = rect;
1074 : }
1075 0 : else if (attr_name == "fixed_h_aspect_ratio")
1076 : {
1077 : float val;
1078 0 : if (!ParseString<float>(this, attr_value, val))
1079 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1080 : else
1081 0 : image->m_FixedHAspectRatio = val;
1082 : }
1083 0 : else if (attr_name == "round_coordinates")
1084 : {
1085 : bool b;
1086 0 : if (!ParseString<bool>(this, attr_value, b))
1087 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1088 : else
1089 0 : image->m_RoundCoordinates = b;
1090 : }
1091 0 : else if (attr_name == "wrap_mode")
1092 : {
1093 0 : if (attr_value == L"repeat")
1094 0 : image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::REPEAT;
1095 0 : else if (attr_value == L"mirrored_repeat")
1096 0 : image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT;
1097 0 : else if (attr_value == L"clamp_to_edge")
1098 0 : image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE;
1099 : else
1100 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1101 : }
1102 0 : else if (attr_name == "backcolor")
1103 : {
1104 0 : if (!ParseString<CGUIColor>(this, attr_value, image->m_BackColor))
1105 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
1106 : }
1107 : else
1108 0 : debug_warn(L"Invalid data - DTD shouldn't allow this");
1109 : }
1110 :
1111 : // Look for effects
1112 0 : for (XMBElement child : element.GetChildNodes())
1113 : {
1114 0 : std::string_view ElementName(xmb.GetElementStringView(child.GetNodeName()));
1115 0 : if (ElementName == "effect")
1116 : {
1117 0 : if (image->m_Effects)
1118 0 : LOGERROR("GUI <image> must not have more than one <effect>");
1119 : else
1120 : {
1121 0 : image->m_Effects = std::make_shared<SGUIImageEffects>();
1122 0 : Xeromyces_ReadEffects(xmb, child, *image->m_Effects);
1123 : }
1124 : }
1125 : else
1126 0 : debug_warn(L"Invalid data - DTD shouldn't allow this");
1127 : }
1128 :
1129 0 : parent.AddImage(std::move(image));
1130 0 : }
1131 :
1132 0 : void CGUI::Xeromyces_ReadEffects(const XMBData& xmb, XMBElement element, SGUIImageEffects& effects)
1133 : {
1134 0 : for (XMBAttribute attr : element.GetAttributes())
1135 : {
1136 0 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1137 0 : if (attr_name == "add_color")
1138 : {
1139 0 : if (!effects.m_AddColor.ParseString(*this, attr.Value, 0))
1140 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr.Value);
1141 : }
1142 0 : else if (attr_name == "grayscale")
1143 0 : effects.m_Greyscale = true;
1144 : else
1145 0 : debug_warn(L"Invalid data - DTD shouldn't allow this");
1146 : }
1147 0 : }
1148 :
1149 5 : void CGUI::Xeromyces_ReadStyle(const XMBData& xmb, XMBElement element)
1150 : {
1151 10 : SGUIStyle style;
1152 10 : CStr name;
1153 :
1154 10 : for (XMBAttribute attr : element.GetAttributes())
1155 : {
1156 5 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1157 : // The "name" setting is actually the name of the style
1158 : // and not a new default
1159 5 : if (attr_name == "name")
1160 5 : name = attr.Value;
1161 : else
1162 0 : style.m_SettingsDefaults.emplace(std::string(attr_name), attr.Value.FromUTF8());
1163 : }
1164 :
1165 5 : m_Styles.erase(name);
1166 5 : m_Styles.emplace(name, std::move(style));
1167 5 : }
1168 :
1169 0 : void CGUI::Xeromyces_ReadScrollBarStyle(const XMBData& xmb, XMBElement element)
1170 : {
1171 0 : SGUIScrollBarStyle scrollbar;
1172 0 : CStr name;
1173 :
1174 : // Setup some defaults.
1175 0 : scrollbar.m_MinimumBarSize = 0.f;
1176 : // Using 1.0e10 as a substitute for infinity
1177 0 : scrollbar.m_MaximumBarSize = 1.0e10;
1178 0 : scrollbar.m_UseEdgeButtons = false;
1179 :
1180 0 : for (XMBAttribute attr : element.GetAttributes())
1181 : {
1182 0 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1183 0 : CStr attr_value(attr.Value);
1184 :
1185 0 : if (attr_value == "null")
1186 0 : continue;
1187 :
1188 0 : if (attr_name == "name")
1189 0 : name = attr_value;
1190 0 : else if (attr_name == "show_edge_buttons")
1191 : {
1192 : bool b;
1193 0 : if (!ParseString<bool>(this, attr_value.FromUTF8(), b))
1194 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1195 : else
1196 0 : scrollbar.m_UseEdgeButtons = b;
1197 : }
1198 0 : else if (attr_name == "width")
1199 : {
1200 : float f;
1201 0 : if (!ParseString<float>(this, attr_value.FromUTF8(), f))
1202 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1203 : else
1204 0 : scrollbar.m_Width = f;
1205 : }
1206 0 : else if (attr_name == "minimum_bar_size")
1207 : {
1208 : float f;
1209 0 : if (!ParseString<float>(this, attr_value.FromUTF8(), f))
1210 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1211 : else
1212 0 : scrollbar.m_MinimumBarSize = f;
1213 : }
1214 0 : else if (attr_name == "maximum_bar_size")
1215 : {
1216 : float f;
1217 0 : if (!ParseString<float>(this, attr_value.FromUTF8(), f))
1218 0 : LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
1219 : else
1220 0 : scrollbar.m_MaximumBarSize = f;
1221 : }
1222 0 : else if (attr_name == "sprite_button_top")
1223 0 : scrollbar.m_SpriteButtonTop = attr_value;
1224 0 : else if (attr_name == "sprite_button_top_pressed")
1225 0 : scrollbar.m_SpriteButtonTopPressed = attr_value;
1226 0 : else if (attr_name == "sprite_button_top_disabled")
1227 0 : scrollbar.m_SpriteButtonTopDisabled = attr_value;
1228 0 : else if (attr_name == "sprite_button_top_over")
1229 0 : scrollbar.m_SpriteButtonTopOver = attr_value;
1230 0 : else if (attr_name == "sprite_button_bottom")
1231 0 : scrollbar.m_SpriteButtonBottom = attr_value;
1232 0 : else if (attr_name == "sprite_button_bottom_pressed")
1233 0 : scrollbar.m_SpriteButtonBottomPressed = attr_value;
1234 0 : else if (attr_name == "sprite_button_bottom_disabled")
1235 0 : scrollbar.m_SpriteButtonBottomDisabled = attr_value;
1236 0 : else if (attr_name == "sprite_button_bottom_over")
1237 0 : scrollbar.m_SpriteButtonBottomOver = attr_value;
1238 0 : else if (attr_name == "sprite_back_vertical")
1239 0 : scrollbar.m_SpriteBackVertical = attr_value;
1240 0 : else if (attr_name == "sprite_bar_vertical")
1241 0 : scrollbar.m_SpriteBarVertical = attr_value;
1242 0 : else if (attr_name == "sprite_bar_vertical_over")
1243 0 : scrollbar.m_SpriteBarVerticalOver = attr_value;
1244 0 : else if (attr_name == "sprite_bar_vertical_pressed")
1245 0 : scrollbar.m_SpriteBarVerticalPressed = attr_value;
1246 : }
1247 :
1248 0 : m_ScrollBarStyles.erase(name);
1249 0 : m_ScrollBarStyles.emplace(name, std::move(scrollbar));
1250 0 : }
1251 :
1252 0 : void CGUI::Xeromyces_ReadIcon(const XMBData& xmb, XMBElement element)
1253 : {
1254 0 : SGUIIcon icon;
1255 0 : CStr name;
1256 :
1257 0 : for (XMBAttribute attr : element.GetAttributes())
1258 : {
1259 0 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1260 0 : CStr attr_value(attr.Value);
1261 :
1262 0 : if (attr_value == "null")
1263 0 : continue;
1264 :
1265 0 : if (attr_name == "name")
1266 0 : name = attr_value;
1267 0 : else if (attr_name == "sprite")
1268 0 : icon.m_SpriteName = attr_value;
1269 0 : else if (attr_name == "size")
1270 : {
1271 0 : CSize2D size;
1272 0 : if (!ParseString<CSize2D>(this, attr_value.FromUTF8(), size))
1273 0 : LOGERROR("Error parsing '%s' (\"%s\") inside <icon>.", attr_name, attr_value);
1274 : else
1275 0 : icon.m_Size = size;
1276 : }
1277 : else
1278 0 : debug_warn(L"Invalid data - DTD shouldn't allow this");
1279 : }
1280 :
1281 0 : m_Icons.erase(name);
1282 0 : m_Icons.emplace(name, std::move(icon));
1283 0 : }
1284 :
1285 0 : void CGUI::Xeromyces_ReadTooltip(const XMBData& xmb, XMBElement element)
1286 : {
1287 0 : IGUIObject* object = new CTooltip(*this);
1288 :
1289 0 : for (XMBAttribute attr : element.GetAttributes())
1290 : {
1291 0 : std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
1292 0 : CStr attr_value(attr.Value);
1293 :
1294 0 : if (attr_name == "name")
1295 0 : object->SetName("__tooltip_" + attr_value);
1296 : else
1297 0 : object->SetSettingFromString(std::string(attr_name), attr_value.FromUTF8(), true);
1298 : }
1299 :
1300 0 : if (!AddObject(*m_BaseObject, *object))
1301 0 : delete object;
1302 0 : }
1303 :
1304 0 : void CGUI::Xeromyces_ReadColor(const XMBData& xmb, XMBElement element)
1305 : {
1306 0 : XMBAttributeList attributes = element.GetAttributes();
1307 0 : CStr name = attributes.GetNamedItem(xmb.GetAttributeID("name"));
1308 :
1309 : // Try parsing value
1310 0 : CStr value(element.GetText());
1311 0 : if (value.empty())
1312 0 : return;
1313 :
1314 0 : CColor color;
1315 0 : if (color.ParseString(value))
1316 : {
1317 0 : m_PreDefinedColors.erase(name);
1318 0 : m_PreDefinedColors.emplace(
1319 : std::piecewise_construct,
1320 0 : std::forward_as_tuple(name),
1321 0 : std::forward_as_tuple(color.r, color.g, color.b, color.a));
1322 : }
1323 : else
1324 0 : LOGERROR("GUI: Unable to create custom color '%s'. Invalid color syntax.", name.c_str());
1325 3 : }
|