1 /* Copyright (C) 2022 Wildfire Games.
2 * This file is part of 0 A.D.
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.
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.
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/>.
18 #include "precompiled.h"
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"
33 #include "lib/input.h"
34 #include "lib/sysdep/sysdep.h"
35 #include "lib/timer.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"
51 #include <unordered_map>
52 #include <unordered_set>
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
57 const CStr
CGUI::EventNameLoad
= "Load";
58 const CStr
CGUI::EventNameTick
= "Tick";
59 const CStr
CGUI::EventNamePress
= "Press";
60 const CStr
CGUI::EventNameKeyDown
= "KeyDown";
61 const CStr
CGUI::EventNameRelease
= "Release";
62 const CStr
CGUI::EventNameMouseRightPress
= "MouseRightPress";
63 const CStr
CGUI::EventNameMouseLeftPress
= "MouseLeftPress";
64 const CStr
CGUI::EventNameMouseWheelDown
= "MouseWheelDown";
65 const CStr
CGUI::EventNameMouseWheelUp
= "MouseWheelUp";
66 const CStr
CGUI::EventNameMouseLeftDoubleClick
= "MouseLeftDoubleClick";
67 const CStr
CGUI::EventNameMouseLeftRelease
= "MouseLeftRelease";
68 const CStr
CGUI::EventNameMouseRightDoubleClick
= "MouseRightDoubleClick";
69 const CStr
CGUI::EventNameMouseRightRelease
= "MouseRightRelease";
77 // Index of the object in a depth-first search inside GUI tree.
79 // Cached value of GetBufferedZ to avoid recursive calls in a deep hierarchy.
83 template<class Container
>
84 void CollectVisibleObjectsRecursively(const std::vector
<IGUIObject
*>& objects
, Container
* visibleObjects
)
86 for (IGUIObject
* const& object
: objects
)
88 if (!object
->IsHidden())
90 visibleObjects
->emplace_back(VisibleObject
{object
, static_cast<u32
>(visibleObjects
->size()), 0.0f
});
91 CollectVisibleObjectsRecursively(object
->GetChildren(), visibleObjects
);
96 } // anonynous namespace
98 CGUI::CGUI(const std::shared_ptr
<ScriptContext
>& context
)
99 : m_BaseObject(std::make_unique
<CGUIDummyObject
>(*this)),
100 m_FocusedObject(nullptr),
101 m_InternalNameNumber(0),
104 m_ScriptInterface
= std::make_shared
<ScriptInterface
>("Engine", "GUIPage", context
);
105 m_ScriptInterface
->SetCallbackData(this);
107 GuiScriptingInit(*m_ScriptInterface
);
108 m_ScriptInterface
->LoadGlobalScripts();
113 for (const std::pair
<const CStr
, IGUIObject
*>& p
: m_pAllObjects
)
117 InReaction
CGUI::HandleEvent(const SDL_Event_
* ev
)
119 InReaction ret
= IN_PASS
;
121 if (ev
->ev
.type
== SDL_HOTKEYDOWN
|| ev
->ev
.type
== SDL_HOTKEYPRESS
|| ev
->ev
.type
== SDL_HOTKEYUP
)
123 const char* hotkey
= static_cast<const char*>(ev
->ev
.user
.data1
);
125 const CStr
& eventName
= ev
->ev
.type
== SDL_HOTKEYPRESS
? EventNamePress
: ev
->ev
.type
== SDL_HOTKEYDOWN
? EventNameKeyDown
: EventNameRelease
;
127 if (m_GlobalHotkeys
.find(hotkey
) != m_GlobalHotkeys
.end() && m_GlobalHotkeys
[hotkey
].find(eventName
) != m_GlobalHotkeys
[hotkey
].end())
131 ScriptRequest
rq(m_ScriptInterface
);
132 JS::RootedObject
globalObj(rq
.cx
, rq
.glob
);
133 JS::RootedValue
result(rq
.cx
);
134 if (!JS_CallFunctionValue(rq
.cx
, globalObj
, m_GlobalHotkeys
[hotkey
][eventName
], JS::HandleValueArray::empty(), &result
))
135 ScriptException::CatchPending(rq
);
138 std::map
<CStr
, std::vector
<IGUIObject
*> >::iterator it
= m_HotkeyObjects
.find(hotkey
);
139 if (it
!= m_HotkeyObjects
.end())
140 for (IGUIObject
* const& obj
: it
->second
)
142 if (!obj
->IsEnabled())
144 if (ev
->ev
.type
== SDL_HOTKEYPRESS
)
145 ret
= obj
->SendEvent(GUIM_PRESSED
, EventNamePress
);
146 else if (ev
->ev
.type
== SDL_HOTKEYDOWN
)
147 ret
= obj
->SendEvent(GUIM_KEYDOWN
, EventNameKeyDown
);
149 ret
= obj
->SendEvent(GUIM_RELEASED
, EventNameRelease
);
153 else if (ev
->ev
.type
== SDL_MOUSEMOTION
)
155 // Yes the mouse position is stored as float to avoid
156 // constant conversions when operating in a
157 // float-based environment.
158 m_MousePos
= CVector2D((float)ev
->ev
.motion
.x
/ g_VideoMode
.GetScale(), (float)ev
->ev
.motion
.y
/ g_VideoMode
.GetScale());
160 SGUIMessage
msg(GUIM_MOUSE_MOTION
);
161 m_BaseObject
->RecurseObject(&IGUIObject::IsHiddenOrGhost
, &IGUIObject::HandleMessage
, msg
);
164 // Update m_MouseButtons. (BUTTONUP is handled later.)
165 else if (ev
->ev
.type
== SDL_MOUSEBUTTONDOWN
)
167 switch (ev
->ev
.button
.button
)
169 case SDL_BUTTON_LEFT
:
170 case SDL_BUTTON_RIGHT
:
171 case SDL_BUTTON_MIDDLE
:
172 m_MouseButtons
|= Bit
<unsigned int>(ev
->ev
.button
.button
);
179 // Update m_MousePos (for delayed mouse button events)
180 CVector2D oldMousePos
= m_MousePos
;
181 if (ev
->ev
.type
== SDL_MOUSEBUTTONDOWN
|| ev
->ev
.type
== SDL_MOUSEBUTTONUP
)
183 m_MousePos
= CVector2D((float)ev
->ev
.button
.x
/ g_VideoMode
.GetScale(), (float)ev
->ev
.button
.y
/ g_VideoMode
.GetScale());
186 // Allow the focused object to pre-empt regular GUI events.
187 if (GetFocusedObject())
188 ret
= GetFocusedObject()->PreemptEvent(ev
);
190 // Only one object can be hovered
191 // pNearest will after this point at the hovered object, possibly nullptr
192 IGUIObject
* pNearest
= FindObjectUnderMouse();
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 m_BaseObject
->RecurseObject(&IGUIObject::IsHiddenOrGhost
, &IGUIObject::UpdateMouseOver
, static_cast<IGUIObject
* const&>(pNearest
));
201 if (ev
->ev
.type
== SDL_MOUSEBUTTONDOWN
)
203 switch (ev
->ev
.button
.button
)
205 case SDL_BUTTON_LEFT
:
206 // Focus the clicked object (or focus none if nothing clicked on)
207 SetFocusedObject(pNearest
);
210 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT
, EventNameMouseLeftPress
);
213 case SDL_BUTTON_RIGHT
:
215 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT
, EventNameMouseRightPress
);
222 else if (ev
->ev
.type
== SDL_MOUSEWHEEL
&& pNearest
)
224 if (ev
->ev
.wheel
.y
< 0)
225 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_WHEEL_DOWN
, EventNameMouseWheelDown
);
226 else if (ev
->ev
.wheel
.y
> 0)
227 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_WHEEL_UP
, EventNameMouseWheelUp
);
229 else if (ev
->ev
.type
== SDL_MOUSEBUTTONUP
)
231 switch (ev
->ev
.button
.button
)
233 case SDL_BUTTON_LEFT
:
236 double timeElapsed
= timer_Time() - pNearest
->m_LastClickTime
[SDL_BUTTON_LEFT
];
237 pNearest
->m_LastClickTime
[SDL_BUTTON_LEFT
] = timer_Time();
238 if (timeElapsed
< SELECT_DBLCLICK_RATE
)
239 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT
, EventNameMouseLeftDoubleClick
);
241 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_RELEASE_LEFT
, EventNameMouseLeftRelease
);
244 case SDL_BUTTON_RIGHT
:
247 double timeElapsed
= timer_Time() - pNearest
->m_LastClickTime
[SDL_BUTTON_RIGHT
];
248 pNearest
->m_LastClickTime
[SDL_BUTTON_RIGHT
] = timer_Time();
249 if (timeElapsed
< SELECT_DBLCLICK_RATE
)
250 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_DBLCLICK_RIGHT
, EventNameMouseRightDoubleClick
);
252 ret
= pNearest
->SendMouseEvent(GUIM_MOUSE_RELEASE_RIGHT
, EventNameMouseRightRelease
);
257 // Reset all states on all visible objects
258 m_BaseObject
->RecurseObject(&IGUIObject::IsHidden
, &IGUIObject::ResetStates
);
260 // Since the hover state will have been reset, we reload it.
261 m_BaseObject
->RecurseObject(&IGUIObject::IsHiddenOrGhost
, &IGUIObject::UpdateMouseOver
, static_cast<IGUIObject
* const&>(pNearest
));
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 if (ev
->ev
.type
== SDL_MOUSEBUTTONUP
)
270 switch (ev
->ev
.button
.button
)
272 case SDL_BUTTON_LEFT
:
273 case SDL_BUTTON_RIGHT
:
274 case SDL_BUTTON_MIDDLE
:
275 m_MouseButtons
&= ~Bit
<unsigned int>(ev
->ev
.button
.button
);
282 // Restore m_MousePos (for delayed mouse button events)
283 if (ev
->ev
.type
== SDL_MOUSEBUTTONDOWN
|| ev
->ev
.type
== SDL_MOUSEBUTTONUP
)
284 m_MousePos
= oldMousePos
;
286 // Let GUI items handle keys after everything else, e.g. for input boxes.
287 if (ret
== IN_PASS
&& GetFocusedObject())
289 if (ev
->ev
.type
== SDL_KEYUP
|| ev
->ev
.type
== SDL_KEYDOWN
||
290 ev
->ev
.type
== SDL_HOTKEYUP
|| ev
->ev
.type
== SDL_HOTKEYDOWN
||
291 ev
->ev
.type
== SDL_TEXTINPUT
|| ev
->ev
.type
== SDL_TEXTEDITING
)
292 ret
= GetFocusedObject()->ManuallyHandleKeys(ev
);
293 // else will return IN_PASS because we never used the button.
299 void CGUI::TickObjects()
301 m_BaseObject
->RecurseObject(&IGUIObject::IsHiddenOrGhost
, &IGUIObject::Tick
);
302 SendEventToAll(EventNameTick
);
303 m_Tooltip
.Update(FindObjectUnderMouse(), m_MousePos
, *this);
306 void CGUI::SendEventToAll(const CStr
& eventName
)
308 std::unordered_map
<CStr
, std::vector
<IGUIObject
*>>::iterator it
= m_EventObjects
.find(eventName
);
309 if (it
== m_EventObjects
.end())
312 std::vector
<IGUIObject
*> copy
= it
->second
;
313 for (IGUIObject
* object
: copy
)
314 object
->ScriptEvent(eventName
);
317 void CGUI::SendEventToAll(const CStr
& eventName
, const JS::HandleValueArray
& paramData
)
319 std::unordered_map
<CStr
, std::vector
<IGUIObject
*>>::iterator it
= m_EventObjects
.find(eventName
);
320 if (it
== m_EventObjects
.end())
323 std::vector
<IGUIObject
*> copy
= it
->second
;
324 for (IGUIObject
* object
: copy
)
325 object
->ScriptEvent(eventName
, paramData
);
328 void CGUI::Draw(CCanvas2D
& canvas
)
330 using Arena
= Allocators::DynamicArena
<128 * KiB
>;
331 using ObjectListAllocator
= ProxyAllocator
<VisibleObject
, Arena
>;
334 std::vector
<VisibleObject
, ObjectListAllocator
> visibleObjects((ObjectListAllocator(arena
)));
335 CollectVisibleObjectsRecursively(m_BaseObject
->GetChildren(), &visibleObjects
);
336 for (VisibleObject
& visibleObject
: visibleObjects
)
337 visibleObject
.bufferedZ
= visibleObject
.object
->GetBufferedZ();
339 std::sort(visibleObjects
.begin(), visibleObjects
.end(), [](const VisibleObject
& visibleObject1
, const VisibleObject
& visibleObject2
) -> bool {
340 if (visibleObject1
.bufferedZ
!= visibleObject2
.bufferedZ
)
341 return visibleObject1
.bufferedZ
< visibleObject2
.bufferedZ
;
342 return visibleObject1
.index
< visibleObject2
.index
;
345 for (const VisibleObject
& visibleObject
: visibleObjects
)
346 visibleObject
.object
->Draw(canvas
);
349 void CGUI::DrawSprite(const CGUISpriteInstance
& Sprite
, CCanvas2D
& canvas
, const CRect
& Rect
, const CRect
& UNUSED(Clipping
))
351 // If the sprite doesn't exist (name == ""), don't bother drawing anything
357 Sprite
.Draw(*this, canvas
, Rect
, m_Sprites
);
360 void CGUI::UpdateResolution()
362 m_BaseObject
->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize
);
365 IGUIObject
* CGUI::ConstructObject(const CStr
& str
)
367 std::map
<CStr
, ConstructObjectFunction
>::iterator it
= m_ObjectTypes
.find(str
);
369 if (it
== m_ObjectTypes
.end())
372 return (*it
->second
)(*this);
375 bool CGUI::AddObject(IGUIObject
& parent
, IGUIObject
& child
)
377 if (child
.m_Name
.empty())
379 LOGERROR("Can't register an object without name!");
383 if (m_pAllObjects
.find(child
.m_Name
) != m_pAllObjects
.end())
385 LOGERROR("Can't register more than one object of the name %s", child
.m_Name
.c_str());
389 m_pAllObjects
[child
.m_Name
] = &child
;
390 parent
.RegisterChild(&child
);
394 IGUIObject
* CGUI::GetBaseObject()
396 return m_BaseObject
.get();
399 bool CGUI::ObjectExists(const CStr
& Name
) const
401 return m_pAllObjects
.find(Name
) != m_pAllObjects
.end();
404 IGUIObject
* CGUI::FindObjectByName(const CStr
& Name
) const
406 map_pObjects::const_iterator it
= m_pAllObjects
.find(Name
);
408 if (it
== m_pAllObjects
.end())
414 IGUIObject
* CGUI::FindObjectUnderMouse()
416 IGUIObject
* pNearest
= nullptr;
417 m_BaseObject
->RecurseObject(&IGUIObject::IsHiddenOrGhost
, &IGUIObject::ChooseMouseOverAndClosest
, pNearest
);
421 CSize2D
CGUI::GetWindowSize() const
423 return CSize2D
{static_cast<float>(g_xres
) / g_VideoMode
.GetScale(), static_cast<float>(g_yres
) / g_VideoMode
.GetScale() };
426 void CGUI::SendFocusMessage(EGUIMessageType msgType
)
430 SGUIMessage
msg(msgType
);
431 m_FocusedObject
->HandleMessage(msg
);
435 void CGUI::SetFocusedObject(IGUIObject
* pObject
)
437 if (pObject
== m_FocusedObject
)
442 SGUIMessage
msg(GUIM_LOST_FOCUS
);
443 m_FocusedObject
->HandleMessage(msg
);
446 m_FocusedObject
= pObject
;
450 SGUIMessage
msg(GUIM_GOT_FOCUS
);
451 m_FocusedObject
->HandleMessage(msg
);
455 void CGUI::SetObjectStyle(IGUIObject
* pObject
, const CStr
& styleName
)
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 pObject
->ApplyStyle(styleName
);
462 void CGUI::UnsetObjectStyle(IGUIObject
* pObject
)
464 SetObjectStyle(pObject
, "default");
467 void CGUI::SetObjectHotkey(IGUIObject
* pObject
, const CStr
& hotkeyTag
)
469 if (!hotkeyTag
.empty())
470 m_HotkeyObjects
[hotkeyTag
].push_back(pObject
);
473 void CGUI::UnsetObjectHotkey(IGUIObject
* pObject
, const CStr
& hotkeyTag
)
475 if (hotkeyTag
.empty())
478 std::vector
<IGUIObject
*>& assignment
= m_HotkeyObjects
[hotkeyTag
];
484 [&pObject
](const IGUIObject
* hotkeyObject
)
485 { return pObject
== hotkeyObject
; }),
489 void CGUI::SetGlobalHotkey(const CStr
& hotkeyTag
, const CStr
& eventName
, JS::HandleValue function
)
491 ScriptRequest
rq(*m_ScriptInterface
);
493 if (hotkeyTag
.empty())
495 ScriptException::Raise(rq
, "Cannot assign a function to an empty hotkey identifier!");
499 // Only support "Press", "Keydown" and "Release" events.
500 if (eventName
!= EventNamePress
&& eventName
!= EventNameKeyDown
&& eventName
!= EventNameRelease
)
502 ScriptException::Raise(rq
, "Cannot assign a function to an unsupported event!");
506 if (!function
.isObject() || !JS_ObjectIsFunction(&function
.toObject()))
508 ScriptException::Raise(rq
, "Cannot assign non-function value to global hotkey '%s'", hotkeyTag
.c_str());
512 UnsetGlobalHotkey(hotkeyTag
, eventName
);
513 m_GlobalHotkeys
[hotkeyTag
][eventName
].init(rq
.cx
, function
);
516 void CGUI::UnsetGlobalHotkey(const CStr
& hotkeyTag
, const CStr
& eventName
)
518 std::map
<CStr
, std::map
<CStr
, JS::PersistentRootedValue
>>::iterator it
= m_GlobalHotkeys
.find(hotkeyTag
);
519 if (it
== m_GlobalHotkeys
.end())
522 m_GlobalHotkeys
[hotkeyTag
].erase(eventName
);
524 if (m_GlobalHotkeys
.count(hotkeyTag
) == 0)
525 m_GlobalHotkeys
.erase(it
);
528 const SGUIScrollBarStyle
* CGUI::GetScrollBarStyle(const CStr
& style
) const
530 std::map
<CStr
, const SGUIScrollBarStyle
>::const_iterator it
= m_ScrollBarStyles
.find(style
);
531 if (it
== m_ScrollBarStyles
.end())
540 void CGUI::LoadXmlFile(const VfsPath
& Filename
, std::unordered_set
<VfsPath
>& Paths
)
542 Paths
.insert(Filename
);
545 if (xeroFile
.Load(g_VFS
, Filename
, "gui") != PSRETURN_OK
)
546 // The error has already been reported by CXeromyces
549 XMBElement node
= xeroFile
.GetRoot();
550 std::string_view
root_name(xeroFile
.GetElementStringView(node
.GetNodeName()));
552 if (root_name
== "objects")
553 Xeromyces_ReadRootObjects(xeroFile
, node
, Paths
);
554 else if (root_name
== "sprites")
555 Xeromyces_ReadRootSprites(xeroFile
, node
);
556 else if (root_name
== "styles")
557 Xeromyces_ReadRootStyles(xeroFile
, node
);
558 else if (root_name
== "setup")
559 Xeromyces_ReadRootSetup(xeroFile
, node
);
561 LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name
.data());
564 void CGUI::LoadedXmlFiles()
566 m_BaseObject
->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize
);
568 SGUIMessage
msg(GUIM_LOAD
);
569 m_BaseObject
->RecurseObject(nullptr, &IGUIObject::HandleMessage
, msg
);
571 SendEventToAll(EventNameLoad
);
574 //===================================================================
575 // XML Reading Xeromyces Specific Sub-Routines
576 //===================================================================
578 void CGUI::Xeromyces_ReadRootObjects(const XMBData
& xmb
, XMBElement element
, std::unordered_set
<VfsPath
>& Paths
)
580 int el_script
= xmb
.GetElementID("script");
582 std::vector
<std::pair
<CStr
, CStr
> > subst
;
584 // Iterate main children
585 // they should all be <object> or <script> elements
586 for (XMBElement child
: element
.GetChildNodes())
588 if (child
.GetNodeName() == el_script
)
589 // Execute the inline script
590 Xeromyces_ReadScript(xmb
, child
, Paths
);
592 // Read in this whole object into the GUI
593 Xeromyces_ReadObject(xmb
, child
, m_BaseObject
.get(), subst
, Paths
, 0);
597 void CGUI::Xeromyces_ReadRootSprites(const XMBData
& xmb
, XMBElement element
)
599 for (XMBElement child
: element
.GetChildNodes())
600 Xeromyces_ReadSprite(xmb
, child
);
603 void CGUI::Xeromyces_ReadRootStyles(const XMBData
& xmb
, XMBElement element
)
605 for (XMBElement child
: element
.GetChildNodes())
606 Xeromyces_ReadStyle(xmb
, child
);
609 void CGUI::Xeromyces_ReadRootSetup(const XMBData
& xmb
, XMBElement element
)
611 for (XMBElement child
: element
.GetChildNodes())
613 std::string_view
name(xmb
.GetElementStringView(child
.GetNodeName()));
614 if (name
== "scrollbar")
615 Xeromyces_ReadScrollBarStyle(xmb
, child
);
616 else if (name
== "icon")
617 Xeromyces_ReadIcon(xmb
, child
);
618 else if (name
== "tooltip")
619 Xeromyces_ReadTooltip(xmb
, child
);
620 else if (name
== "color")
621 Xeromyces_ReadColor(xmb
, child
);
623 debug_warn(L
"Invalid data - DTD shouldn't allow this");
627 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
)
631 XMBAttributeList attributes
= element
.GetAttributes();
633 CStr
type(attributes
.GetNamedItem(xmb
.GetAttributeID("type")));
637 // Construct object from specified type
638 // henceforth, we need to do a rollback before aborting.
639 // i.e. releasing this object
640 IGUIObject
* object
= ConstructObject(type
);
644 LOGERROR("GUI: Unrecognized object type \"%s\"", type
.c_str());
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)
655 ELMT(translatableAttribute
);
671 // Read Style and set defaults
673 // If the setting "style" is set, try loading that setting.
675 // Always load default (if it's available) first!
677 SetObjectStyle(object
, "default");
679 CStr
argStyle(attributes
.GetNamedItem(attr_style
));
680 if (!argStyle
.empty())
681 SetObjectStyle(object
, argStyle
);
683 bool NameSet
= false;
684 bool ManuallySetZ
= false;
686 for (XMBAttribute attr
: attributes
)
688 // If value is "null", then it is equivalent as never being entered
689 if (attr
.Value
== "null")
692 // Ignore "type" and "style", we've already checked it
693 if (attr
.Name
== attr_type
|| attr
.Name
== attr_style
)
696 if (attr
.Name
== attr_name
)
698 CStr
name(attr
.Value
);
700 if (name
.Left(2) == "__")
702 LOGERROR("GUI: Names starting with '__' are reserved for the engine (object: %s)", name
.c_str());
706 for (const std::pair
<CStr
, CStr
>& sub
: NameSubst
)
707 name
.Replace(sub
.first
, sub
.second
);
709 object
->SetName(name
);
714 if (attr
.Name
== attr_z
)
717 object
->SetSettingFromString(xmb
.GetAttributeString(attr
.Name
), attr
.Value
.FromUTF8(), false);
720 // Check if name isn't set, generate an internal name in that case.
723 object
->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber
) + ")");
724 ++m_InternalNameNumber
;
727 CStrW
caption(element
.GetText().FromUTF8());
728 if (!caption
.empty())
729 object
->SetSettingFromString("caption", caption
, false);
731 for (XMBElement child
: element
.GetChildNodes())
733 // Check what name the elements got
734 int element_name
= child
.GetNodeName();
736 if (element_name
== elmt_object
)
738 // Call this function on the child
739 Xeromyces_ReadObject(xmb
, child
, object
, NameSubst
, Paths
, nesting_depth
);
741 else if (element_name
== elmt_action
)
743 // Scripted <action> element
745 // Check for a 'file' parameter
746 CStrW
filename(child
.GetAttributes().GetNamedItem(attr_file
).FromUTF8());
750 // If there is a file, open it and use it as the code
751 if (!filename
.empty())
753 Paths
.insert(filename
);
755 if (scriptfile
.Load(g_VFS
, filename
) != PSRETURN_OK
)
757 LOGERROR("Error opening GUI script action file '%s'", utf8_from_wstring(filename
));
761 code
= scriptfile
.DecodeUTF8(); // assume it's UTF-8
764 XMBElementList grandchildren
= child
.GetChildNodes();
765 if (!grandchildren
.empty()) // The <action> element contains <keep> and <translate> tags.
766 for (XMBElement grandchild
: grandchildren
)
768 if (grandchild
.GetNodeName() == elmt_translate
)
769 code
+= g_L10n
.Translate(grandchild
.GetText());
770 else if (grandchild
.GetNodeName() == elmt_keep
)
771 code
+= grandchild
.GetText();
773 else // It's pure JavaScript code.
774 // Read the inline code (concatenating to the file code, if both are specified)
775 code
+= CStr(child
.GetText());
777 CStr eventName
= child
.GetAttributes().GetNamedItem(attr_on
);
778 object
->RegisterScriptHandler(eventName
, code
, *this);
780 else if (child
.GetNodeName() == elmt_script
)
782 Xeromyces_ReadScript(xmb
, child
, Paths
);
784 else if (element_name
== elmt_repeat
)
786 Xeromyces_ReadRepeat(xmb
, child
, object
, NameSubst
, Paths
, nesting_depth
);
788 else if (element_name
== elmt_translatableAttribute
)
790 // This is an element in the form "<translatableAttribute id="attributeName">attributeValue</translatableAttribute>".
791 CStr
attributeName(child
.GetAttributes().GetNamedItem(attr_id
)); // Read the attribute name.
792 if (attributeName
.empty())
794 LOGERROR("GUI: 'translatableAttribute' XML element with empty 'id' XML attribute found. (object: %s)", object
->GetPresentableName().c_str());
798 CStr
value(child
.GetText());
802 CStr
context(child
.GetAttributes().GetNamedItem(attr_context
)); // Read the context if any.
804 CStr translatedValue
= context
.empty() ?
805 g_L10n
.Translate(value
) :
806 g_L10n
.TranslateWithContext(context
, value
);
808 object
->SetSettingFromString(attributeName
, translatedValue
.FromUTF8(), false);
810 else if (element_name
== elmt_attribute
)
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 CStr
attributeName(child
.GetAttributes().GetNamedItem(attr_id
)); // Read the attribute name.
815 if (attributeName
.empty())
817 LOGERROR("GUI: 'attribute' XML element with empty 'id' XML attribute found. (object: %s)", object
->GetPresentableName().c_str());
821 CStr translatedValue
;
823 for (XMBElement grandchild
: child
.GetChildNodes())
825 if (grandchild
.GetNodeName() == elmt_translate
)
826 translatedValue
+= g_L10n
.Translate(grandchild
.GetText());
827 else if (grandchild
.GetNodeName() == elmt_keep
)
828 translatedValue
+= grandchild
.GetText();
830 object
->SetSettingFromString(attributeName
, translatedValue
.FromUTF8(), false);
832 else if (element_name
== elmt_include
)
834 CStrW
filename(child
.GetAttributes().GetNamedItem(attr_file
).FromUTF8());
835 CStrW
directory(child
.GetAttributes().GetNamedItem(attr_directory
).FromUTF8());
836 if (!filename
.empty())
838 if (!directory
.empty())
839 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
));
841 Paths
.insert(filename
);
843 CXeromyces xeroIncluded
;
844 if (xeroIncluded
.Load(g_VFS
, filename
, "gui") != PSRETURN_OK
)
846 LOGERROR("GUI: Error reading included XML: '%s'", utf8_from_wstring(filename
));
850 XMBElement node
= xeroIncluded
.GetRoot();
851 if (node
.GetNodeName() != xeroIncluded
.GetElementID("object"))
853 LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", utf8_from_wstring(filename
));
857 if (nesting_depth
+1 >= MAX_OBJECT_DEPTH
)
859 LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(filename
));
863 Xeromyces_ReadObject(xeroIncluded
, node
, object
, NameSubst
, Paths
, nesting_depth
+1);
865 else if (!directory
.empty())
867 if (nesting_depth
+1 >= MAX_OBJECT_DEPTH
)
869 LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(directory
));
874 vfs::GetPathnames(g_VFS
, directory
, L
"*.xml", pathnames
);
875 for (const VfsPath
& path
: pathnames
)
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
880 CXeromyces xeroIncluded
;
881 if (xeroIncluded
.Load(g_VFS
, path
, "gui") != PSRETURN_OK
)
883 LOGERROR("GUI: Error reading included XML: '%s'", path
.string8());
887 XMBElement node
= xeroIncluded
.GetRoot();
888 if (node
.GetNodeName() != xeroIncluded
.GetElementID("object"))
890 LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", path
.string8());
893 Xeromyces_ReadObject(xeroIncluded
, node
, object
, NameSubst
, Paths
, nesting_depth
+1);
898 LOGERROR("GUI: 'include' XML element must have valid 'file' or 'directory' attribute found. (object %s)", object
->GetPresentableName().c_str());
902 // Try making the object read the tag.
903 if (!object
->HandleAdditionalChildren(xmb
, child
))
904 LOGERROR("GUI: (object: %s) Reading unknown children for its type", object
->GetPresentableName().c_str());
908 object
->AdditionalChildrenHandled();
912 // Set it automatically to 10 plus its parents
913 if (object
->m_Absolute
)
914 // If the object is absolute, we'll have to get the parent's Z buffered,
916 object
->m_Z
.Set(pParent
->GetBufferedZ() + 10.f
, false);
918 // If the object is relative, then we'll just store Z as "10"
919 object
->m_Z
.Set(10.f
, false);
922 if (!AddObject(*pParent
, *object
))
930 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
)
932 #define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
933 #define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
938 XMBAttributeList attributes
= element
.GetAttributes();
940 int count
= CStr(attributes
.GetNamedItem(attr_count
)).ToInt();
941 CStr
var("["+attributes
.GetNamedItem(attr_var
)+"]");
945 for (int n
= 0; n
< count
; ++n
)
947 NameSubst
.emplace_back(var
, "[" + CStr::FromInt(n
) + "]");
949 XERO_ITER_EL(element
, child
)
951 if (child
.GetNodeName() == elmt_object
)
952 Xeromyces_ReadObject(xmb
, child
, pParent
, NameSubst
, Paths
, nesting_depth
);
954 NameSubst
.pop_back();
958 void CGUI::Xeromyces_ReadScript(const XMBData
& xmb
, XMBElement element
, std::unordered_set
<VfsPath
>& Paths
)
960 // Check for a 'file' parameter
961 CStrW
fileAttr(element
.GetAttributes().GetNamedItem(xmb
.GetAttributeID("file")).FromUTF8());
963 // If there is a file specified, open and execute it
964 if (!fileAttr
.empty())
966 if (!VfsPath(fileAttr
).IsDirectory())
968 Paths
.insert(fileAttr
);
969 m_ScriptInterface
->LoadGlobalScriptFile(fileAttr
);
972 LOGERROR("GUI: Script path %s is not a file path", fileAttr
.ToUTF8().c_str());
975 // If it has a directory attribute, read all JS files in that directory
976 CStrW
directoryAttr(element
.GetAttributes().GetNamedItem(xmb
.GetAttributeID("directory")).FromUTF8());
977 if (!directoryAttr
.empty())
979 if (VfsPath(directoryAttr
).IsDirectory())
982 vfs::GetPathnames(g_VFS
, directoryAttr
, L
"*.js", pathnames
);
983 for (const VfsPath
& path
: pathnames
)
985 // Only load new files (so when the insert succeeds)
986 if (Paths
.insert(path
).second
)
987 m_ScriptInterface
->LoadGlobalScriptFile(path
);
991 LOGERROR("GUI: Script path %s is not a directory path", directoryAttr
.ToUTF8().c_str());
994 CStr
code(element
.GetText());
996 m_ScriptInterface
->LoadGlobalScript(L
"Some XML file", code
);
999 void CGUI::Xeromyces_ReadSprite(const XMBData
& xmb
, XMBElement element
)
1001 auto sprite
= std::make_unique
<CGUISprite
>();
1003 // Get name, we know it exists because of DTD requirements
1004 CStr name
= element
.GetAttributes().GetNamedItem(xmb
.GetAttributeID("name"));
1006 if (m_Sprites
.find(name
) != m_Sprites
.end())
1007 LOGWARNING("GUI sprite name '%s' used more than once; first definition will be discarded", name
.c_str());
1009 // shared_ptr to link the effect to every image, faster than copy.
1010 std::shared_ptr
<SGUIImageEffects
> effects
;
1012 for (XMBElement child
: element
.GetChildNodes())
1014 std::string_view
ElementName(xmb
.GetElementStringView(child
.GetNodeName()));
1015 if (ElementName
== "image")
1016 Xeromyces_ReadImage(xmb
, child
, *sprite
);
1017 else if (ElementName
== "effect")
1020 LOGERROR("GUI <sprite> must not have more than one <effect>");
1023 effects
= std::make_shared
<SGUIImageEffects
>();
1024 Xeromyces_ReadEffects(xmb
, child
, *effects
);
1028 debug_warn(L
"Invalid data - DTD shouldn't allow this");
1031 // Apply the effects to every image (unless the image overrides it with
1032 // different effects)
1035 for (const std::unique_ptr
<SGUIImage
>& image
: sprite
->m_Images
)
1036 if (!image
->m_Effects
)
1037 image
->m_Effects
= effects
;
1040 m_Sprites
.erase(name
);
1041 m_Sprites
.emplace(name
, std::move(sprite
));
1044 void CGUI::Xeromyces_ReadImage(const XMBData
& xmb
, XMBElement element
, CGUISprite
& parent
)
1046 auto image
= std::make_unique
<SGUIImage
>();
1048 // TODO Gee: Setup defaults here (or maybe they are in the SGUIImage ctor)
1050 for (XMBAttribute attr
: element
.GetAttributes())
1052 std::string_view
attr_name(xmb
.GetAttributeStringView(attr
.Name
));
1053 CStrW
attr_value(attr
.Value
.FromUTF8());
1055 if (attr_name
== "texture")
1057 image
->m_TextureName
= VfsPath("art/textures/ui") / attr_value
;
1059 else if (attr_name
== "size")
1061 image
->m_Size
.FromString(attr
.Value
);
1063 else if (attr_name
== "texture_size")
1065 image
->m_TextureSize
.FromString(attr
.Value
);
1067 else if (attr_name
== "real_texture_placement")
1070 if (!ParseString
<CRect
>(this, attr_value
, rect
))
1071 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, utf8_from_wstring(attr_value
));
1073 image
->m_TexturePlacementInFile
= rect
;
1075 else if (attr_name
== "fixed_h_aspect_ratio")
1078 if (!ParseString
<float>(this, attr_value
, val
))
1079 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, utf8_from_wstring(attr_value
));
1081 image
->m_FixedHAspectRatio
= val
;
1083 else if (attr_name
== "round_coordinates")
1086 if (!ParseString
<bool>(this, attr_value
, b
))
1087 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, utf8_from_wstring(attr_value
));
1089 image
->m_RoundCoordinates
= b
;
1091 else if (attr_name
== "wrap_mode")
1093 if (attr_value
== L
"repeat")
1094 image
->m_AddressMode
= Renderer::Backend::Sampler::AddressMode::REPEAT
;
1095 else if (attr_value
== L
"mirrored_repeat")
1096 image
->m_AddressMode
= Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT
;
1097 else if (attr_value
== L
"clamp_to_edge")
1098 image
->m_AddressMode
= Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE
;
1100 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, utf8_from_wstring(attr_value
));
1102 else if (attr_name
== "backcolor")
1104 if (!ParseString
<CGUIColor
>(this, attr_value
, image
->m_BackColor
))
1105 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, utf8_from_wstring(attr_value
));
1108 debug_warn(L
"Invalid data - DTD shouldn't allow this");
1112 for (XMBElement child
: element
.GetChildNodes())
1114 std::string_view
ElementName(xmb
.GetElementStringView(child
.GetNodeName()));
1115 if (ElementName
== "effect")
1117 if (image
->m_Effects
)
1118 LOGERROR("GUI <image> must not have more than one <effect>");
1121 image
->m_Effects
= std::make_shared
<SGUIImageEffects
>();
1122 Xeromyces_ReadEffects(xmb
, child
, *image
->m_Effects
);
1126 debug_warn(L
"Invalid data - DTD shouldn't allow this");
1129 parent
.AddImage(std::move(image
));
1132 void CGUI::Xeromyces_ReadEffects(const XMBData
& xmb
, XMBElement element
, SGUIImageEffects
& effects
)
1134 for (XMBAttribute attr
: element
.GetAttributes())
1136 std::string_view
attr_name(xmb
.GetAttributeStringView(attr
.Name
));
1137 if (attr_name
== "add_color")
1139 if (!effects
.m_AddColor
.ParseString(*this, attr
.Value
, 0))
1140 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, attr
.Value
);
1142 else if (attr_name
== "grayscale")
1143 effects
.m_Greyscale
= true;
1145 debug_warn(L
"Invalid data - DTD shouldn't allow this");
1149 void CGUI::Xeromyces_ReadStyle(const XMBData
& xmb
, XMBElement element
)
1154 for (XMBAttribute attr
: element
.GetAttributes())
1156 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 if (attr_name
== "name")
1162 style
.m_SettingsDefaults
.emplace(std::string(attr_name
), attr
.Value
.FromUTF8());
1165 m_Styles
.erase(name
);
1166 m_Styles
.emplace(name
, std::move(style
));
1169 void CGUI::Xeromyces_ReadScrollBarStyle(const XMBData
& xmb
, XMBElement element
)
1171 SGUIScrollBarStyle scrollbar
;
1174 // Setup some defaults.
1175 scrollbar
.m_MinimumBarSize
= 0.f
;
1176 // Using 1.0e10 as a substitute for infinity
1177 scrollbar
.m_MaximumBarSize
= 1.0e10
;
1178 scrollbar
.m_UseEdgeButtons
= false;
1180 for (XMBAttribute attr
: element
.GetAttributes())
1182 std::string_view
attr_name(xmb
.GetAttributeStringView(attr
.Name
));
1183 CStr
attr_value(attr
.Value
);
1185 if (attr_value
== "null")
1188 if (attr_name
== "name")
1190 else if (attr_name
== "show_edge_buttons")
1193 if (!ParseString
<bool>(this, attr_value
.FromUTF8(), b
))
1194 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, attr_value
);
1196 scrollbar
.m_UseEdgeButtons
= b
;
1198 else if (attr_name
== "width")
1201 if (!ParseString
<float>(this, attr_value
.FromUTF8(), f
))
1202 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, attr_value
);
1204 scrollbar
.m_Width
= f
;
1206 else if (attr_name
== "minimum_bar_size")
1209 if (!ParseString
<float>(this, attr_value
.FromUTF8(), f
))
1210 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, attr_value
);
1212 scrollbar
.m_MinimumBarSize
= f
;
1214 else if (attr_name
== "maximum_bar_size")
1217 if (!ParseString
<float>(this, attr_value
.FromUTF8(), f
))
1218 LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name
, attr_value
);
1220 scrollbar
.m_MaximumBarSize
= f
;
1222 else if (attr_name
== "sprite_button_top")
1223 scrollbar
.m_SpriteButtonTop
= attr_value
;
1224 else if (attr_name
== "sprite_button_top_pressed")
1225 scrollbar
.m_SpriteButtonTopPressed
= attr_value
;
1226 else if (attr_name
== "sprite_button_top_disabled")
1227 scrollbar
.m_SpriteButtonTopDisabled
= attr_value
;
1228 else if (attr_name
== "sprite_button_top_over")
1229 scrollbar
.m_SpriteButtonTopOver
= attr_value
;
1230 else if (attr_name
== "sprite_button_bottom")
1231 scrollbar
.m_SpriteButtonBottom
= attr_value
;
1232 else if (attr_name
== "sprite_button_bottom_pressed")
1233 scrollbar
.m_SpriteButtonBottomPressed
= attr_value
;
1234 else if (attr_name
== "sprite_button_bottom_disabled")
1235 scrollbar
.m_SpriteButtonBottomDisabled
= attr_value
;
1236 else if (attr_name
== "sprite_button_bottom_over")
1237 scrollbar
.m_SpriteButtonBottomOver
= attr_value
;
1238 else if (attr_name
== "sprite_back_vertical")
1239 scrollbar
.m_SpriteBackVertical
= attr_value
;
1240 else if (attr_name
== "sprite_bar_vertical")
1241 scrollbar
.m_SpriteBarVertical
= attr_value
;
1242 else if (attr_name
== "sprite_bar_vertical_over")
1243 scrollbar
.m_SpriteBarVerticalOver
= attr_value
;
1244 else if (attr_name
== "sprite_bar_vertical_pressed")
1245 scrollbar
.m_SpriteBarVerticalPressed
= attr_value
;
1248 m_ScrollBarStyles
.erase(name
);
1249 m_ScrollBarStyles
.emplace(name
, std::move(scrollbar
));
1252 void CGUI::Xeromyces_ReadIcon(const XMBData
& xmb
, XMBElement element
)
1257 for (XMBAttribute attr
: element
.GetAttributes())
1259 std::string_view
attr_name(xmb
.GetAttributeStringView(attr
.Name
));
1260 CStr
attr_value(attr
.Value
);
1262 if (attr_value
== "null")
1265 if (attr_name
== "name")
1267 else if (attr_name
== "sprite")
1268 icon
.m_SpriteName
= attr_value
;
1269 else if (attr_name
== "size")
1272 if (!ParseString
<CSize2D
>(this, attr_value
.FromUTF8(), size
))
1273 LOGERROR("Error parsing '%s' (\"%s\") inside <icon>.", attr_name
, attr_value
);
1278 debug_warn(L
"Invalid data - DTD shouldn't allow this");
1281 m_Icons
.erase(name
);
1282 m_Icons
.emplace(name
, std::move(icon
));
1285 void CGUI::Xeromyces_ReadTooltip(const XMBData
& xmb
, XMBElement element
)
1287 IGUIObject
* object
= new CTooltip(*this);
1289 for (XMBAttribute attr
: element
.GetAttributes())
1291 std::string_view
attr_name(xmb
.GetAttributeStringView(attr
.Name
));
1292 CStr
attr_value(attr
.Value
);
1294 if (attr_name
== "name")
1295 object
->SetName("__tooltip_" + attr_value
);
1297 object
->SetSettingFromString(std::string(attr_name
), attr_value
.FromUTF8(), true);
1300 if (!AddObject(*m_BaseObject
, *object
))
1304 void CGUI::Xeromyces_ReadColor(const XMBData
& xmb
, XMBElement element
)
1306 XMBAttributeList attributes
= element
.GetAttributes();
1307 CStr name
= attributes
.GetNamedItem(xmb
.GetAttributeID("name"));
1309 // Try parsing value
1310 CStr
value(element
.GetText());
1315 if (color
.ParseString(value
))
1317 m_PreDefinedColors
.erase(name
);
1318 m_PreDefinedColors
.emplace(
1319 std::piecewise_construct
,
1320 std::forward_as_tuple(name
),
1321 std::forward_as_tuple(color
.r
, color
.g
, color
.b
, color
.a
));
1324 LOGERROR("GUI: Unable to create custom color '%s'. Invalid color syntax.", name
.c_str());