LCOV - code coverage report
Current view: top level - source/gui - CGUI.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 171 664 25.8 %
Date: 2022-06-14 00:41:00 Functions: 19 45 42.2 %

          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             : 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";
      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          11 : CGUI::CGUI(const std::shared_ptr<ScriptContext>& context)
      99          33 :     : m_BaseObject(std::make_unique<CGUIDummyObject>(*this)),
     100             :       m_FocusedObject(nullptr),
     101             :       m_InternalNameNumber(0),
     102          33 :       m_MouseButtons(0)
     103             : {
     104          11 :     m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "GUIPage", context);
     105          22 :     m_ScriptInterface->SetCallbackData(this);
     106             : 
     107          22 :     GuiScriptingInit(*m_ScriptInterface);
     108          22 :     m_ScriptInterface->LoadGlobalScripts();
     109          11 : }
     110             : 
     111          22 : CGUI::~CGUI()
     112             : {
     113          37 :     for (const std::pair<const CStr, IGUIObject*>& p : m_pAllObjects)
     114           4 :         delete p.second;
     115          11 : }
     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           8 :         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           8 :         if (it != m_HotkeyObjects.end())
     140           0 :             for (IGUIObject* const& obj : it->second)
     141             :             {
     142           0 :                 if (!obj->IsEnabled())
     143             :                     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             :             }
     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           0 :         case SDL_BUTTON_RIGHT:
     171           0 :         case SDL_BUTTON_MIDDLE:
     172           0 :             m_MouseButtons |= Bit<unsigned int>(ev->ev.button.button);
     173           0 :             break;
     174             :         default:
     175             :             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          14 :     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          14 :         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             :                 break;
     212             : 
     213           0 :             case SDL_BUTTON_RIGHT:
     214           0 :                 if (pNearest)
     215           0 :                     ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT, EventNameMouseRightPress);
     216             :                 break;
     217             : 
     218             :             default:
     219             :                 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             :                 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             :                 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           0 :         case SDL_BUTTON_RIGHT:
     274           0 :         case SDL_BUTTON_MIDDLE:
     275           0 :             m_MouseButtons &= ~Bit<unsigned int>(ev->ev.button.button);
     276           0 :             break;
     277             :         default:
     278             :             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           4 :     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          16 :     if (it == m_EventObjects.end())
     310           6 :         return;
     311             : 
     312           8 :     std::vector<IGUIObject*> copy = it->second;
     313          11 :     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           0 :     using Arena = Allocators::DynamicArena<128 * KiB>;
     331           0 :     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             :         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           8 :     if (it == m_ObjectTypes.end())
     370             :         return nullptr;
     371             : 
     372           8 :     return (*it->second)(*this);
     373             : }
     374             : 
     375           4 : bool CGUI::AddObject(IGUIObject& parent, IGUIObject& child)
     376             : {
     377           8 :     if (child.m_Name.empty())
     378             :     {
     379           0 :         LOGERROR("Can't register an object without name!");
     380           0 :         return false;
     381             :     }
     382             : 
     383           8 :     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         122 :     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          10 :     if (it == m_pAllObjects.end())
     409             :         return nullptr;
     410             : 
     411          10 :     return it->second;
     412             : }
     413             : 
     414          16 : IGUIObject* CGUI::FindObjectUnderMouse()
     415             : {
     416          16 :     IGUIObject* pNearest = nullptr;
     417          32 :     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             :         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           0 : 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           0 :     pObject->ApplyStyle(styleName);
     460           0 : }
     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             :         return;
     477             : 
     478           0 :     std::vector<IGUIObject*>& assignment = m_HotkeyObjects[hotkeyTag];
     479             : 
     480           0 :     assignment.erase(
     481             :         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             :         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             :         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             :         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             :         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          24 :     CXeromyces xeroFile;
     545          16 :     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          12 :     m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
     567             : 
     568          12 :     SGUIMessage msg(GUIM_LOAD);
     569          12 :     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           3 :     std::vector<std::pair<CStr, CStr> > subst;
     583             : 
     584             :     // Iterate main children
     585             :     //  they should all be <object> or <script> elements
     586          30 :     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           8 :             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          20 :     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           8 :     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           4 :     CStr argStyle(attributes.GetNamedItem(attr_style));
     680           8 :     if (!argStyle.empty())
     681           0 :         SetObjectStyle(object, argStyle);
     682             : 
     683           4 :     bool NameSet = false;
     684           4 :     bool ManuallySetZ = false;
     685             : 
     686          20 :     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           4 :             continue;
     691             : 
     692             :         // Ignore "type" and "style", we've already checked it
     693           4 :         if (attr.Name == attr_type || attr.Name == attr_style)
     694             :             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          12 :             for (const std::pair<CStr, CStr>& sub : NameSubst)
     707           0 :                 name.Replace(sub.first, sub.second);
     708             : 
     709           4 :             object->SetName(name);
     710             :             NameSet = true;
     711             :             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           8 :     if (!caption.empty())
     729           0 :         object->SetSettingFromString("caption", caption, false);
     730             : 
     731           8 :     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             :     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           3 :     CStrW fileAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("file")).FromUTF8());
     962             : 
     963             :     // If there is a file specified, open and execute it
     964           6 :     if (!fileAttr.empty())
     965             :     {
     966           9 :         if (!VfsPath(fileAttr).IsDirectory())
     967             :         {
     968           3 :             Paths.insert(fileAttr);
     969           9 :             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           6 :     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           6 :     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           0 :             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           0 :             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           5 :     SGUIStyle style;
    1152          10 :     CStr name;
    1153             : 
    1154          20 :     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           0 :             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           0 :             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           0 :             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           0 :             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             : }

Generated by: LCOV version 1.13