mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 13:23:56 -07:00
Some checks failed
checkrefs / lfscheck (push) Has been cancelled
checkrefs / checkrefs (push) Has been cancelled
lint / cppcheck (push) Has been cancelled
lint / copyright (push) Has been cancelled
lint / jenkinsfiles (push) Has been cancelled
pre-commit / build (push) Has been cancelled
This shifts the responsibility of updating the actual size more towards IGUIObject, and enables only ever doing it when the value is actually needed. This allows us to remove the delay of size changed notifications, since the value is now already recalculated as infrequently as possible anyways. All of that ensures that the actual size (returned by GetActualSize) is always up-to-date e.g. when reading it from the parent, which was previously broken. Fixes #8200
1374 lines
42 KiB
C++
1374 lines
42 KiB
C++
/* Copyright (C) 2026 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "CGUI.h"
|
|
|
|
#include "graphics/Canvas2D.h"
|
|
#include "graphics/Color.h"
|
|
#include "gui/CGUISetting.h"
|
|
#include "gui/CGUISprite.h"
|
|
#include "gui/GUIObjectEventBroadcaster.h"
|
|
#include "gui/IGUIScrollBar.h"
|
|
#include "gui/ObjectBases/IGUIObject.h"
|
|
#include "gui/ObjectTypes/CGUIDummyObject.h"
|
|
#include "gui/ObjectTypes/CTooltip.h"
|
|
#include "gui/Scripting/JSInterface_GUIProxy.h"
|
|
#include "gui/Scripting/ScriptFunctions.h"
|
|
#include "gui/SettingTypes/CGUISize.h"
|
|
#include "i18n/L10n.h"
|
|
#include "lib/bits.h"
|
|
#include "lib/debug.h"
|
|
#include "lib/external_libraries/libsdl.h"
|
|
#include "lib/file/vfs/vfs_util.h"
|
|
#include "lib/input.h"
|
|
#include "lib/path.h"
|
|
#include "lib/timer.h"
|
|
#include "lib/utf8.h"
|
|
#include "maths/Size2D.h"
|
|
#include "network/NetClient.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/Errors.h"
|
|
#include "ps/Filesystem.h"
|
|
#include "ps/GameSetup/Config.h"
|
|
#include "ps/Hotkey.h"
|
|
#include "ps/VideoMode.h"
|
|
#include "ps/XMB/XMBData.h"
|
|
#include "ps/XML/Xeromyces.h"
|
|
#include "renderer/backend/Sampler.h"
|
|
#include "scriptinterface/FunctionWrapper.h"
|
|
#include "scriptinterface/Object.h"
|
|
#include "scriptinterface/ScriptExceptions.h"
|
|
#include "scriptinterface/ScriptInterface.h"
|
|
#include "scriptinterface/ScriptRequest.h"
|
|
|
|
#include <algorithm>
|
|
#include <js/CallAndConstruct.h>
|
|
#include <js/Promise.h>
|
|
#include <optional>
|
|
#include <SDL_events.h>
|
|
#include <SDL_mouse.h>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
|
|
const double SELECT_DBLCLICK_RATE = 0.5;
|
|
const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion
|
|
|
|
const CStr CGUI::EventNameLoad = "Load";
|
|
const CStr CGUI::EventNameTick = "Tick";
|
|
const CStr CGUI::EventNamePress = "Press";
|
|
const CStr CGUI::EventNameKeyDown = "KeyDown";
|
|
const CStr CGUI::EventNameRelease = "Release";
|
|
const CStr CGUI::EventNameMouseRightPress = "MouseRightPress";
|
|
const CStr CGUI::EventNameMouseLeftPress = "MouseLeftPress";
|
|
const CStr CGUI::EventNameMouseWheelDown = "MouseWheelDown";
|
|
const CStr CGUI::EventNameMouseWheelUp = "MouseWheelUp";
|
|
const CStr CGUI::EventNameMouseWheelLeft = "MouseWheelLeft";
|
|
const CStr CGUI::EventNameMouseWheelRight = "MouseWheelRight";
|
|
const CStr CGUI::EventNameMouseLeftDoubleClick = "MouseLeftDoubleClick";
|
|
const CStr CGUI::EventNameMouseLeftRelease = "MouseLeftRelease";
|
|
const CStr CGUI::EventNameMouseRightDoubleClick = "MouseRightDoubleClick";
|
|
const CStr CGUI::EventNameMouseRightRelease = "MouseRightRelease";
|
|
|
|
CGUI::CGUI(ScriptContext& context)
|
|
: m_BaseObject(std::make_unique<CGUIDummyObject>(*this)),
|
|
m_FocusedObject(nullptr),
|
|
m_InternalNameNumber(0),
|
|
m_MouseButtons(0)
|
|
{
|
|
m_ScriptInterface = std::make_shared<ScriptInterface>("Engine", "GUIPage", context,
|
|
[](const VfsPath& path){
|
|
return path.string8().find("gui/") == 0;
|
|
});
|
|
m_ScriptInterface->SetCallbackData(this);
|
|
|
|
GuiScriptingInit(*m_ScriptInterface);
|
|
m_ScriptInterface->LoadGlobalScripts();
|
|
}
|
|
|
|
CGUI::~CGUI()
|
|
{
|
|
if (g_NetClient)
|
|
g_NetClient->Unregister(*m_ScriptInterface);
|
|
}
|
|
|
|
InReaction CGUI::HandleEvent(const SDL_Event_* ev)
|
|
{
|
|
InReaction ret = IN_PASS;
|
|
|
|
if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYUP)
|
|
{
|
|
const char* hotkey = static_cast<const char*>(ev->ev.user.data1);
|
|
|
|
const CStr& eventName = ev->ev.type == SDL_HOTKEYPRESS ? EventNamePress : ev->ev.type == SDL_HOTKEYDOWN ? EventNameKeyDown : EventNameRelease;
|
|
|
|
if (m_GlobalHotkeys.find(hotkey) != m_GlobalHotkeys.end() && m_GlobalHotkeys[hotkey].find(eventName) != m_GlobalHotkeys[hotkey].end())
|
|
{
|
|
ret = IN_HANDLED;
|
|
|
|
ScriptRequest rq(m_ScriptInterface);
|
|
JS::RootedObject globalObj(rq.cx, rq.glob);
|
|
JS::RootedValue result(rq.cx);
|
|
if (!JS_CallFunctionValue(rq.cx, globalObj, m_GlobalHotkeys[hotkey][eventName], JS::HandleValueArray::empty(), &result))
|
|
ScriptException::CatchPending(rq);
|
|
}
|
|
|
|
std::map<CStr, std::vector<IGUIObject*> >::iterator it = m_HotkeyObjects.find(hotkey);
|
|
if (it != m_HotkeyObjects.end())
|
|
for (IGUIObject* const& obj : it->second)
|
|
{
|
|
if (!obj->IsEnabled())
|
|
continue;
|
|
if (ev->ev.type == SDL_HOTKEYPRESS)
|
|
ret = obj->SendEvent(GUIM_PRESSED, EventNamePress);
|
|
else if (ev->ev.type == SDL_HOTKEYDOWN)
|
|
ret = obj->SendEvent(GUIM_KEYDOWN, EventNameKeyDown);
|
|
else
|
|
ret = obj->SendEvent(GUIM_RELEASED, EventNameRelease);
|
|
}
|
|
}
|
|
|
|
else if (ev->ev.type == SDL_MOUSEMOTION)
|
|
{
|
|
// Yes the mouse position is stored as float to avoid
|
|
// constant conversions when operating in a
|
|
// float-based environment.
|
|
m_MousePos = CVector2D((float)ev->ev.motion.x / g_VideoMode.GetScale(), (float)ev->ev.motion.y / g_VideoMode.GetScale());
|
|
|
|
SGUIMessage msg(GUIM_MOUSE_MOTION);
|
|
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhostOrOutOfBoundaries, &IGUIObject::HandleMessage, msg);
|
|
}
|
|
|
|
// Update m_MouseButtons. (BUTTONUP is handled later.)
|
|
else if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
|
|
{
|
|
switch (ev->ev.button.button)
|
|
{
|
|
case SDL_BUTTON_LEFT:
|
|
case SDL_BUTTON_RIGHT:
|
|
case SDL_BUTTON_MIDDLE:
|
|
m_MouseButtons |= Bit<unsigned int>(ev->ev.button.button);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Update m_MousePos (for delayed mouse button events)
|
|
CVector2D oldMousePos = m_MousePos;
|
|
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
|
|
{
|
|
m_MousePos = CVector2D((float)ev->ev.button.x / g_VideoMode.GetScale(), (float)ev->ev.button.y / g_VideoMode.GetScale());
|
|
}
|
|
|
|
// Allow the focused object to pre-empt regular GUI events.
|
|
if (GetFocusedObject())
|
|
ret = GetFocusedObject()->PreemptEvent(ev);
|
|
|
|
// Only one object can be hovered
|
|
// pNearest will after this point at the hovered object, possibly nullptr
|
|
IGUIObject* pNearest = FindObjectUnderMouse();
|
|
|
|
if (ret == IN_PASS)
|
|
{
|
|
// Now we'll call UpdateMouseOver on *all* objects,
|
|
// we'll input the one hovered, and they will each
|
|
// update their own data and send messages accordingly
|
|
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhostOrOutOfBoundaries, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
|
|
|
|
if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
|
|
{
|
|
switch (ev->ev.button.button)
|
|
{
|
|
case SDL_BUTTON_LEFT:
|
|
// Focus the clicked object (or focus none if nothing clicked on)
|
|
SetFocusedObject(pNearest);
|
|
|
|
if (pNearest)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT, EventNameMouseLeftPress);
|
|
break;
|
|
|
|
case SDL_BUTTON_RIGHT:
|
|
if (pNearest)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT, EventNameMouseRightPress);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest)
|
|
{
|
|
if (ev->ev.wheel.y < 0)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_DOWN, EventNameMouseWheelDown);
|
|
else if (ev->ev.wheel.y > 0)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_UP, EventNameMouseWheelUp);
|
|
|
|
if (ev->ev.wheel.x < 0)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_LEFT, EventNameMouseWheelLeft);
|
|
else if (ev->ev.wheel.x > 0)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_RIGHT, EventNameMouseWheelRight);
|
|
}
|
|
else if (ev->ev.type == SDL_MOUSEBUTTONUP)
|
|
{
|
|
switch (ev->ev.button.button)
|
|
{
|
|
case SDL_BUTTON_LEFT:
|
|
if (pNearest)
|
|
{
|
|
double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT];
|
|
pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time();
|
|
if (timeElapsed < SELECT_DBLCLICK_RATE)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT, EventNameMouseLeftDoubleClick);
|
|
else
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_LEFT, EventNameMouseLeftRelease);
|
|
}
|
|
break;
|
|
case SDL_BUTTON_RIGHT:
|
|
if (pNearest)
|
|
{
|
|
double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT];
|
|
pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time();
|
|
if (timeElapsed < SELECT_DBLCLICK_RATE)
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_RIGHT, EventNameMouseRightDoubleClick);
|
|
else
|
|
ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_RIGHT, EventNameMouseRightRelease);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Reset all states on all visible objects
|
|
m_BaseObject->RecurseObject(&IGUIObject::IsHidden, &IGUIObject::ResetStates);
|
|
|
|
// Since the hover state will have been reset, we reload it.
|
|
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhostOrOutOfBoundaries, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
|
|
}
|
|
}
|
|
|
|
// BUTTONUP's effect on m_MouseButtons is handled after
|
|
// everything else, so that e.g. 'press' handlers (activated
|
|
// on button up) see which mouse button had been pressed.
|
|
if (ev->ev.type == SDL_MOUSEBUTTONUP)
|
|
{
|
|
switch (ev->ev.button.button)
|
|
{
|
|
case SDL_BUTTON_LEFT:
|
|
case SDL_BUTTON_RIGHT:
|
|
case SDL_BUTTON_MIDDLE:
|
|
m_MouseButtons &= ~Bit<unsigned int>(ev->ev.button.button);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Restore m_MousePos (for delayed mouse button events)
|
|
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
|
|
m_MousePos = oldMousePos;
|
|
|
|
// Let GUI items handle keys after everything else, e.g. for input boxes.
|
|
if (ret == IN_PASS && GetFocusedObject())
|
|
{
|
|
if (ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_KEYDOWN ||
|
|
ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYDOWN ||
|
|
ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING)
|
|
ret = GetFocusedObject()->ManuallyHandleKeys(ev);
|
|
// else will return IN_PASS because we never used the button.
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
JS::Value CGUI::GetHotloadData(const ScriptRequest& rq)
|
|
{
|
|
JS::RootedValue oldNamespace{rq.cx, m_LoadModuleResult.has_value() ?
|
|
JS::ObjectValue(*m_LoadModuleResult->moduleNamespace) : rq.globalValue()};
|
|
JS::RootedValue hotloadDataVal(rq.cx);
|
|
ScriptFunction::Call(rq, oldNamespace, "getHotloadData", &hotloadDataVal);
|
|
return hotloadDataVal;
|
|
}
|
|
|
|
JSObject* CGUI::CallPageInit(const ScriptRequest& rq, Script::StructuredClone initData,
|
|
JS::HandleValue hotloadDataVal, const std::string_view scriptName)
|
|
{
|
|
JS::RootedValue initDataVal{rq.cx};
|
|
if (initData)
|
|
Script::ReadStructuredClone(rq, initData, &initDataVal);
|
|
JS::RootedValue newNamespace{rq.cx, m_LoadModuleResult.has_value() ?
|
|
JS::ObjectValue(*m_LoadModuleResult->moduleNamespace) : rq.globalValue()};
|
|
|
|
if (!Script::HasProperty(rq, newNamespace, "init"))
|
|
return nullptr;
|
|
|
|
JS::RootedValue returnValue{rq.cx};
|
|
if (!ScriptFunction::Call(rq, newNamespace, "init", &returnValue, initDataVal, hotloadDataVal))
|
|
{
|
|
LOGERROR("GUI page '%s': Failed to call init() function", scriptName);
|
|
return nullptr;
|
|
}
|
|
|
|
if (!returnValue.isObject())
|
|
return nullptr;
|
|
|
|
JS::RootedObject returnObject{rq.cx, &returnValue.toObject()};
|
|
if (!JS::IsPromiseObject(returnObject))
|
|
return nullptr;
|
|
|
|
return returnObject;
|
|
}
|
|
|
|
JSObject* CGUI::TickObjects(const ScriptRequest& rq, Script::StructuredClone initData,
|
|
const std::string_view scriptName)
|
|
{
|
|
JS::RootedObject sendingPromise{rq.cx};
|
|
if (m_LoadModuleResult.has_value() && m_LoadModuleResult->iterator->IsDone())
|
|
{
|
|
JS::RootedValue hotloadData{rq.cx, GetHotloadData(rq)};
|
|
m_LoadModuleResult->moduleNamespace = m_LoadModuleResult->iterator->Get();
|
|
++m_LoadModuleResult->iterator;
|
|
sendingPromise = CallPageInit(rq, initData, hotloadData, scriptName);
|
|
}
|
|
|
|
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhostOrOutOfBoundaries, &IGUIObject::Tick);
|
|
SendEventToAll(EventNameTick);
|
|
m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
|
|
return sendingPromise;
|
|
}
|
|
|
|
void CGUI::SendEventToAll(const CStr& eventName,
|
|
const JS::HandleValueArray paramData /* = JS::HandleValueArray::empty() */)
|
|
{
|
|
std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it = m_EventObjects.find(eventName);
|
|
if (it == m_EventObjects.end())
|
|
return;
|
|
|
|
std::vector<IGUIObject*> copy = it->second;
|
|
for (IGUIObject* object : copy)
|
|
object->ScriptEvent(eventName, paramData);
|
|
}
|
|
|
|
void CGUI::Draw(CCanvas2D& canvas)
|
|
{
|
|
CGUIObjectEventBroadcaster::RecurseVisibleObject(m_BaseObject.get(), &IGUIObject::Draw, canvas);
|
|
}
|
|
|
|
void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, CCanvas2D& canvas, const CRect& Rect, const CRect& Clipping)
|
|
{
|
|
// If the sprite doesn't exist (name == ""), don't bother drawing anything
|
|
if (!Sprite)
|
|
return;
|
|
|
|
std::optional<CCanvas2D::ScopedScissor> scopedScissor;
|
|
if (Clipping != CRect())
|
|
scopedScissor.emplace(canvas, Clipping);
|
|
|
|
Sprite.Draw(*this, canvas, Rect, m_Sprites);
|
|
}
|
|
|
|
void CGUI::UpdateResolution()
|
|
{
|
|
m_BaseObject->RecurseObject(nullptr, &IGUIObject::HandleSizeChanged);
|
|
}
|
|
|
|
std::unique_ptr<IGUIObject> CGUI::ConstructObject(const CStr& str)
|
|
{
|
|
std::map<CStr, ConstructObjectFunction>::iterator it = m_ObjectTypes.find(str);
|
|
|
|
if (it == m_ObjectTypes.end())
|
|
return nullptr;
|
|
|
|
return (*it->second)(*this);
|
|
}
|
|
|
|
void CGUI::AddObject(IGUIObject& parent, std::unique_ptr<IGUIObject> child)
|
|
{
|
|
if (child->m_Name.empty())
|
|
{
|
|
LOGERROR("Can't register an object without name!");
|
|
return;
|
|
}
|
|
|
|
if (m_pAllObjects.find(child->m_Name) != m_pAllObjects.end())
|
|
{
|
|
LOGERROR("Can't register more than one object of the name %s", child->m_Name.c_str());
|
|
return;
|
|
}
|
|
|
|
parent.RegisterChild(child.get());
|
|
m_pAllObjects[child->m_Name] = std::move(child);
|
|
}
|
|
|
|
IGUIObject* CGUI::GetBaseObject()
|
|
{
|
|
return m_BaseObject.get();
|
|
};
|
|
|
|
bool CGUI::ObjectExists(const CStr& Name) const
|
|
{
|
|
return m_pAllObjects.find(Name) != m_pAllObjects.end();
|
|
}
|
|
|
|
IGUIObject* CGUI::TryFindObjectByName(const CStr& Name) const
|
|
{
|
|
map_pObjects::const_iterator it = m_pAllObjects.find(Name);
|
|
|
|
if (it == m_pAllObjects.end())
|
|
return nullptr;
|
|
|
|
return it->second.get();
|
|
}
|
|
|
|
IGUIObject* CGUI::FindObjectByName(const CStr& Name) const
|
|
{
|
|
IGUIObject* obj = TryFindObjectByName(Name);
|
|
if (obj == nullptr)
|
|
LOGERROR("Failed to get GUI object by name: object '%s' not found. Note: Use 'Engine.TryGetGUIObjectByName' to query for potentially non-existent objects instead.", Name);
|
|
|
|
return obj;
|
|
}
|
|
|
|
IGUIObject* CGUI::FindObjectUnderMouse()
|
|
{
|
|
IGUIObject* pNearest = nullptr;
|
|
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhostOrOutOfBoundaries, &IGUIObject::ChooseMouseOverAndClosest, pNearest);
|
|
return pNearest;
|
|
}
|
|
|
|
CSize2D CGUI::GetWindowSize() const
|
|
{
|
|
return CSize2D{static_cast<float>(g_xres) / g_VideoMode.GetScale(), static_cast<float>(g_yres) / g_VideoMode.GetScale() };
|
|
}
|
|
|
|
void CGUI::SendFocusMessage(EGUIMessageType msgType)
|
|
{
|
|
if (m_FocusedObject)
|
|
{
|
|
SGUIMessage msg(msgType);
|
|
m_FocusedObject->HandleMessage(msg);
|
|
}
|
|
}
|
|
|
|
void CGUI::SetFocusedObject(IGUIObject* pObject)
|
|
{
|
|
if (pObject == m_FocusedObject)
|
|
return;
|
|
|
|
if (m_FocusedObject)
|
|
{
|
|
SGUIMessage msg(GUIM_LOST_FOCUS);
|
|
m_FocusedObject->HandleMessage(msg);
|
|
}
|
|
|
|
m_FocusedObject = pObject;
|
|
|
|
if (m_FocusedObject)
|
|
{
|
|
SGUIMessage msg(GUIM_GOT_FOCUS);
|
|
m_FocusedObject->HandleMessage(msg);
|
|
}
|
|
}
|
|
|
|
void CGUI::SetObjectStyle(IGUIObject& object, const CStr& styleName)
|
|
{
|
|
// If the style is not recognised (or an empty string) then ApplyStyle will
|
|
// emit an error message. Thus we don't need to handle it here.
|
|
object.ApplyStyle(styleName);
|
|
}
|
|
|
|
void CGUI::UnsetObjectStyle(IGUIObject& object)
|
|
{
|
|
SetObjectStyle(object, "default");
|
|
}
|
|
|
|
void CGUI::SetObjectHotkey(IGUIObject& object, const CStr& hotkeyTag)
|
|
{
|
|
if (!hotkeyTag.empty())
|
|
m_HotkeyObjects[hotkeyTag].push_back(&object);
|
|
}
|
|
|
|
void CGUI::UnsetObjectHotkey(IGUIObject& object, const CStr& hotkeyTag)
|
|
{
|
|
if (hotkeyTag.empty())
|
|
return;
|
|
|
|
std::vector<IGUIObject*>& assignment = m_HotkeyObjects[hotkeyTag];
|
|
|
|
assignment.erase(
|
|
std::remove_if(
|
|
assignment.begin(),
|
|
assignment.end(),
|
|
[&object](const IGUIObject* hotkeyObject)
|
|
{ return &object == hotkeyObject; }),
|
|
assignment.end());
|
|
}
|
|
|
|
void CGUI::SetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName, JS::HandleValue function)
|
|
{
|
|
ScriptRequest rq(*m_ScriptInterface);
|
|
|
|
if (hotkeyTag.empty())
|
|
throw std::invalid_argument{"Cannot assign a function to an empty hotkey identifier!"};
|
|
|
|
// Only support "Press", "Keydown" and "Release" events.
|
|
if (eventName != EventNamePress && eventName != EventNameKeyDown && eventName != EventNameRelease)
|
|
throw std::invalid_argument{"Cannot assign a function to an unsupported event!"};
|
|
|
|
if (!function.isObject() || !JS::IsCallable(&function.toObject()))
|
|
{
|
|
throw std::invalid_argument{fmt::format(
|
|
"Cannot assign non-function value to global hotkey '{}'", hotkeyTag.c_str())};
|
|
}
|
|
|
|
UnsetGlobalHotkey(hotkeyTag, eventName);
|
|
m_GlobalHotkeys[hotkeyTag][eventName].init(rq.cx, function);
|
|
}
|
|
|
|
void CGUI::UnsetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName)
|
|
{
|
|
std::map<CStr, std::map<CStr, JS::PersistentRootedValue>>::iterator it = m_GlobalHotkeys.find(hotkeyTag);
|
|
if (it == m_GlobalHotkeys.end())
|
|
return;
|
|
|
|
m_GlobalHotkeys[hotkeyTag].erase(eventName);
|
|
|
|
if (m_GlobalHotkeys.count(hotkeyTag) == 0)
|
|
m_GlobalHotkeys.erase(it);
|
|
}
|
|
|
|
const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const
|
|
{
|
|
std::map<CStr, const SGUIScrollBarStyle>::const_iterator it = m_ScrollBarStyles.find(style);
|
|
if (it == m_ScrollBarStyles.end())
|
|
return nullptr;
|
|
|
|
return &it->second;
|
|
}
|
|
|
|
/**
|
|
* @callgraph
|
|
*/
|
|
void CGUI::LoadXmlFile(const VfsPath& Filename, std::unordered_set<VfsPath>& Paths)
|
|
{
|
|
Paths.insert(Filename);
|
|
|
|
CXeromyces xeroFile;
|
|
if (xeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK)
|
|
// The error has already been reported by CXeromyces
|
|
return;
|
|
|
|
XMBElement node = xeroFile.GetRoot();
|
|
std::string_view root_name(xeroFile.GetElementStringView(node.GetNodeName()));
|
|
|
|
if (root_name == "objects")
|
|
Xeromyces_ReadRootObjects(xeroFile, node, Paths);
|
|
else if (root_name == "sprites")
|
|
Xeromyces_ReadRootSprites(xeroFile, node);
|
|
else if (root_name == "styles")
|
|
Xeromyces_ReadRootStyles(xeroFile, node);
|
|
else if (root_name == "setup")
|
|
Xeromyces_ReadRootSetup(xeroFile, node);
|
|
else
|
|
LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name.data());
|
|
}
|
|
|
|
void CGUI::LoadedXmlFiles()
|
|
{
|
|
m_BaseObject->RecurseObject(nullptr, &IGUIObject::HandleSizeChanged);
|
|
|
|
SGUIMessage msg(GUIM_LOAD);
|
|
m_BaseObject->RecurseObject(nullptr, &IGUIObject::HandleMessage, msg);
|
|
|
|
SendEventToAll(EventNameLoad);
|
|
}
|
|
|
|
//===================================================================
|
|
// XML Reading Xeromyces Specific Sub-Routines
|
|
//===================================================================
|
|
|
|
void CGUI::Xeromyces_ReadRootObjects(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
|
|
{
|
|
int el_script = xmb.GetElementID("script");
|
|
|
|
std::vector<std::pair<CStr, CStr> > subst;
|
|
|
|
// Iterate main children
|
|
// they should all be <object> or <script> elements
|
|
for (XMBElement child : element.GetChildNodes())
|
|
{
|
|
if (child.GetNodeName() == el_script)
|
|
// Execute the inline script
|
|
Xeromyces_ReadScript(xmb, child, Paths);
|
|
else
|
|
// Read in this whole object into the GUI
|
|
Xeromyces_ReadObject(xmb, child, *m_BaseObject, subst, Paths, 0);
|
|
}
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadRootSprites(const XMBData& xmb, XMBElement element)
|
|
{
|
|
for (XMBElement child : element.GetChildNodes())
|
|
Xeromyces_ReadSprite(xmb, child);
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadRootStyles(const XMBData& xmb, XMBElement element)
|
|
{
|
|
for (XMBElement child : element.GetChildNodes())
|
|
Xeromyces_ReadStyle(xmb, child);
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadRootSetup(const XMBData& xmb, XMBElement element)
|
|
{
|
|
for (XMBElement child : element.GetChildNodes())
|
|
{
|
|
std::string_view name(xmb.GetElementStringView(child.GetNodeName()));
|
|
if (name == "scrollbar")
|
|
Xeromyces_ReadScrollBarStyle(xmb, child);
|
|
else if (name == "icon")
|
|
Xeromyces_ReadIcon(xmb, child);
|
|
else if (name == "tooltip")
|
|
Xeromyces_ReadTooltip(xmb, child);
|
|
else if (name == "color")
|
|
Xeromyces_ReadColor(xmb, child);
|
|
else
|
|
debug_warn(L"Invalid data - DTD shouldn't allow this");
|
|
}
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadObject(const XMBData& xmb, XMBElement element, IGUIObject& parent,
|
|
std::vector<std::pair<CStr, CStr> >& NameSubst, std::unordered_set<VfsPath>& Paths,
|
|
u32 nesting_depth)
|
|
{
|
|
XMBAttributeList attributes = element.GetAttributes();
|
|
|
|
CStr type(attributes.GetNamedItem(xmb.GetAttributeID("type")));
|
|
if (type.empty())
|
|
type = "empty";
|
|
|
|
// Construct object from specified type
|
|
// henceforth, we need to do a rollback before aborting.
|
|
// i.e. releasing this object
|
|
std::unique_ptr<IGUIObject> object = ConstructObject(type);
|
|
|
|
if (!object)
|
|
{
|
|
LOGERROR("GUI: Unrecognized object type \"%s\"", type.c_str());
|
|
return;
|
|
}
|
|
|
|
// Cache some IDs for element attribute names, to avoid string comparisons
|
|
#define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
|
|
#define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
|
|
ELMT(object);
|
|
ELMT(action);
|
|
ELMT(script);
|
|
ELMT(repeat);
|
|
ELMT(translatableAttribute);
|
|
ELMT(translate);
|
|
ELMT(attribute);
|
|
ELMT(keep);
|
|
ELMT(include);
|
|
ATTR(style);
|
|
ATTR(type);
|
|
ATTR(name);
|
|
ATTR(z);
|
|
ATTR(on);
|
|
ATTR(file);
|
|
ATTR(directory);
|
|
ATTR(id);
|
|
ATTR(context);
|
|
|
|
//
|
|
// Read Style and set defaults
|
|
//
|
|
// If the setting "style" is set, try loading that setting.
|
|
//
|
|
// Always load default (if it's available) first!
|
|
//
|
|
SetObjectStyle(*object, "default");
|
|
|
|
CStr argStyle(attributes.GetNamedItem(attr_style));
|
|
if (!argStyle.empty())
|
|
SetObjectStyle(*object, argStyle);
|
|
|
|
bool NameSet = false;
|
|
bool ManuallySetZ = false;
|
|
|
|
for (XMBAttribute attr : attributes)
|
|
{
|
|
// If value is "null", then it is equivalent as never being entered
|
|
if (attr.Value == "null")
|
|
continue;
|
|
|
|
// Ignore "type" and "style", we've already checked it
|
|
if (attr.Name == attr_type || attr.Name == attr_style)
|
|
continue;
|
|
|
|
if (attr.Name == attr_name)
|
|
{
|
|
CStr name(attr.Value);
|
|
|
|
if (name.Left(2) == "__")
|
|
{
|
|
LOGERROR("GUI: Names starting with '__' are reserved for the engine (object: %s)", name.c_str());
|
|
continue;
|
|
}
|
|
|
|
for (const std::pair<CStr, CStr>& sub : NameSubst)
|
|
name.Replace(sub.first, sub.second);
|
|
|
|
object->SetName(name);
|
|
NameSet = true;
|
|
continue;
|
|
}
|
|
|
|
if (attr.Name == attr_z)
|
|
ManuallySetZ = true;
|
|
|
|
object->SetSettingFromString(xmb.GetAttributeString(attr.Name), attr.Value.FromUTF8(), false);
|
|
}
|
|
|
|
// Check if name isn't set, generate an internal name in that case.
|
|
if (!NameSet)
|
|
{
|
|
object->SetName("__internal(" + CStr::FromInt(m_InternalNameNumber) + ")");
|
|
++m_InternalNameNumber;
|
|
}
|
|
|
|
CStrW caption(element.GetText().FromUTF8());
|
|
if (!caption.empty())
|
|
object->SetSettingFromString("caption", caption, false);
|
|
|
|
for (XMBElement child : element.GetChildNodes())
|
|
{
|
|
// Check what name the elements got
|
|
int element_name = child.GetNodeName();
|
|
|
|
if (element_name == elmt_object)
|
|
{
|
|
// Call this function on the child
|
|
Xeromyces_ReadObject(xmb, child, *object, NameSubst, Paths, nesting_depth);
|
|
}
|
|
else if (element_name == elmt_action)
|
|
{
|
|
// Scripted <action> element
|
|
|
|
// Check for a 'file' parameter
|
|
CStrW filename(child.GetAttributes().GetNamedItem(attr_file).FromUTF8());
|
|
|
|
CStr code;
|
|
|
|
// If there is a file, open it and use it as the code
|
|
if (!filename.empty())
|
|
{
|
|
Paths.insert(filename);
|
|
CVFSFile scriptfile;
|
|
if (scriptfile.Load(g_VFS, filename) != PSRETURN_OK)
|
|
{
|
|
LOGERROR("Error opening GUI script action file '%s'", utf8_from_wstring(filename));
|
|
continue;
|
|
}
|
|
|
|
code = scriptfile.DecodeUTF8(); // assume it's UTF-8
|
|
}
|
|
|
|
XMBElementList grandchildren = child.GetChildNodes();
|
|
if (!grandchildren.empty()) // The <action> element contains <keep> and <translate> tags.
|
|
for (XMBElement grandchild : grandchildren)
|
|
{
|
|
if (grandchild.GetNodeName() == elmt_translate)
|
|
code += g_L10n.Translate(grandchild.GetText());
|
|
else if (grandchild.GetNodeName() == elmt_keep)
|
|
code += grandchild.GetText();
|
|
}
|
|
else // It's pure JavaScript code.
|
|
// Read the inline code (concatenating to the file code, if both are specified)
|
|
code += CStr(child.GetText());
|
|
|
|
CStr eventName = child.GetAttributes().GetNamedItem(attr_on);
|
|
object->RegisterScriptHandler(eventName, code, *this);
|
|
}
|
|
else if (child.GetNodeName() == elmt_script)
|
|
{
|
|
Xeromyces_ReadScript(xmb, child, Paths);
|
|
}
|
|
else if (element_name == elmt_repeat)
|
|
{
|
|
Xeromyces_ReadRepeat(xmb, child, *object, NameSubst, Paths, nesting_depth);
|
|
}
|
|
else if (element_name == elmt_translatableAttribute)
|
|
{
|
|
// This is an element in the form "<translatableAttribute id="attributeName">attributeValue</translatableAttribute>".
|
|
CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name.
|
|
if (attributeName.empty())
|
|
{
|
|
LOGERROR("GUI: 'translatableAttribute' XML element with empty 'id' XML attribute found. (object: %s)", object->GetPresentableName().c_str());
|
|
continue;
|
|
}
|
|
|
|
CStr value(child.GetText());
|
|
if (value.empty())
|
|
continue;
|
|
|
|
CStr context(child.GetAttributes().GetNamedItem(attr_context)); // Read the context if any.
|
|
|
|
CStr translatedValue = context.empty() ?
|
|
g_L10n.Translate(value) :
|
|
g_L10n.TranslateWithContext(context, value);
|
|
|
|
object->SetSettingFromString(attributeName, translatedValue.FromUTF8(), false);
|
|
}
|
|
else if (element_name == elmt_attribute)
|
|
{
|
|
// This is an element in the form "<attribute id="attributeName"><keep>Don't translate this part
|
|
// </keep><translate>but translate this one.</translate></attribute>".
|
|
CStr attributeName(child.GetAttributes().GetNamedItem(attr_id)); // Read the attribute name.
|
|
if (attributeName.empty())
|
|
{
|
|
LOGERROR("GUI: 'attribute' XML element with empty 'id' XML attribute found. (object: %s)", object->GetPresentableName().c_str());
|
|
continue;
|
|
}
|
|
|
|
CStr translatedValue;
|
|
|
|
for (XMBElement grandchild : child.GetChildNodes())
|
|
{
|
|
if (grandchild.GetNodeName() == elmt_translate)
|
|
translatedValue += g_L10n.Translate(grandchild.GetText());
|
|
else if (grandchild.GetNodeName() == elmt_keep)
|
|
translatedValue += grandchild.GetText();
|
|
}
|
|
object->SetSettingFromString(attributeName, translatedValue.FromUTF8(), false);
|
|
}
|
|
else if (element_name == elmt_include)
|
|
{
|
|
CStrW filename(child.GetAttributes().GetNamedItem(attr_file).FromUTF8());
|
|
CStrW directory(child.GetAttributes().GetNamedItem(attr_directory).FromUTF8());
|
|
if (!filename.empty())
|
|
{
|
|
if (!directory.empty())
|
|
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));
|
|
|
|
Paths.insert(filename);
|
|
|
|
CXeromyces xeroIncluded;
|
|
if (xeroIncluded.Load(g_VFS, filename, "gui") != PSRETURN_OK)
|
|
{
|
|
LOGERROR("GUI: Error reading included XML: '%s'", utf8_from_wstring(filename));
|
|
continue;
|
|
}
|
|
|
|
XMBElement node = xeroIncluded.GetRoot();
|
|
if (node.GetNodeName() != xeroIncluded.GetElementID("object"))
|
|
{
|
|
LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", utf8_from_wstring(filename));
|
|
continue;
|
|
}
|
|
|
|
if (nesting_depth+1 >= MAX_OBJECT_DEPTH)
|
|
{
|
|
LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(filename));
|
|
continue;
|
|
}
|
|
|
|
Xeromyces_ReadObject(xeroIncluded, node, *object, NameSubst, Paths,
|
|
nesting_depth + 1);
|
|
}
|
|
else if (!directory.empty())
|
|
{
|
|
if (nesting_depth+1 >= MAX_OBJECT_DEPTH)
|
|
{
|
|
LOGERROR("GUI: Too many nested GUI includes. Probably caused by a recursive include attribute. Abort rendering '%s'.", utf8_from_wstring(directory));
|
|
continue;
|
|
}
|
|
|
|
VfsPaths pathnames;
|
|
vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames);
|
|
for (const VfsPath& path : pathnames)
|
|
{
|
|
// as opposed to loading scripts, don't care if it's loaded before
|
|
// one might use the same parts of the GUI in different situations
|
|
Paths.insert(path);
|
|
CXeromyces xeroIncluded;
|
|
if (xeroIncluded.Load(g_VFS, path, "gui") != PSRETURN_OK)
|
|
{
|
|
LOGERROR("GUI: Error reading included XML: '%s'", path.string8());
|
|
continue;
|
|
}
|
|
|
|
XMBElement node = xeroIncluded.GetRoot();
|
|
if (node.GetNodeName() != xeroIncluded.GetElementID("object"))
|
|
{
|
|
LOGERROR("GUI: Error reading included XML: '%s', root element must have be of type 'object'.", path.string8());
|
|
continue;
|
|
}
|
|
Xeromyces_ReadObject(xeroIncluded, node, *object, NameSubst, Paths,
|
|
nesting_depth + 1);
|
|
}
|
|
|
|
}
|
|
else
|
|
LOGERROR("GUI: 'include' XML element must have valid 'file' or 'directory' attribute found. (object %s)", object->GetPresentableName().c_str());
|
|
}
|
|
else
|
|
{
|
|
// Try making the object read the tag.
|
|
if (!object->HandleAdditionalChildren(xmb, child))
|
|
LOGERROR("GUI: (object: %s) Reading unknown children for its type", object->GetPresentableName().c_str());
|
|
}
|
|
}
|
|
|
|
object->AdditionalChildrenHandled();
|
|
|
|
if (!ManuallySetZ)
|
|
{
|
|
// Set it automatically to 10 plus its parents
|
|
if (object->m_Absolute)
|
|
// If the object is absolute, we'll have to get the parent's Z buffered,
|
|
// and add to that!
|
|
object->m_Z.Set(parent.GetBufferedZ() + 10.f, false);
|
|
else
|
|
// If the object is relative, then we'll just store Z as "10"
|
|
object->m_Z.Set(10.f, false);
|
|
}
|
|
|
|
AddObject(parent, std::move(object));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadRepeat(const XMBData& xmb, XMBElement element, IGUIObject& parent,
|
|
std::vector<std::pair<CStr, CStr>>& NameSubst, std::unordered_set<VfsPath>& Paths, u32 nesting_depth)
|
|
{
|
|
#define ELMT(x) int elmt_##x = xmb.GetElementID(#x)
|
|
#define ATTR(x) int attr_##x = xmb.GetAttributeID(#x)
|
|
ELMT(object);
|
|
ATTR(count);
|
|
ATTR(var);
|
|
|
|
XMBAttributeList attributes = element.GetAttributes();
|
|
|
|
int count = CStr(attributes.GetNamedItem(attr_count)).ToInt();
|
|
CStr var("["+attributes.GetNamedItem(attr_var)+"]");
|
|
if (var.size() < 3)
|
|
var = "[n]";
|
|
|
|
for (int n = 0; n < count; ++n)
|
|
{
|
|
NameSubst.emplace_back(var, "[" + CStr::FromInt(n) + "]");
|
|
|
|
XERO_ITER_EL(element, child)
|
|
{
|
|
if (child.GetNodeName() == elmt_object)
|
|
Xeromyces_ReadObject(xmb, child, parent, NameSubst, Paths, nesting_depth);
|
|
}
|
|
NameSubst.pop_back();
|
|
}
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadScript(const XMBData& xmb, XMBElement element, std::unordered_set<VfsPath>& Paths)
|
|
{
|
|
// If there is a "module" attribute, save it so it can be loaded at the end. It isn't saved to `Path`
|
|
// because modules automatically get reloaded.
|
|
const std::string moduleAttribute{element.GetAttributes().GetNamedItem(xmb.GetAttributeID("module"))};
|
|
if (!moduleAttribute.empty())
|
|
{
|
|
if (m_LoadModuleResult.has_value())
|
|
throw std::logic_error{"There can only be one root module per page."};
|
|
|
|
const ScriptRequest rq{m_ScriptInterface};
|
|
m_LoadModuleResult.emplace(rq, moduleAttribute);
|
|
}
|
|
|
|
// If there is a "file" attribute, open and execute it
|
|
CStrW fileAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("file")).FromUTF8());
|
|
if (!fileAttr.empty())
|
|
{
|
|
if (!VfsPath(fileAttr).IsDirectory())
|
|
{
|
|
Paths.insert(fileAttr);
|
|
m_ScriptInterface->LoadGlobalScriptFile(fileAttr);
|
|
}
|
|
else
|
|
LOGERROR("GUI: Script path %s is not a file path", fileAttr.ToUTF8().c_str());
|
|
}
|
|
|
|
// If there is a "directory" attribute, read all JS files in that directory
|
|
CStrW directoryAttr(element.GetAttributes().GetNamedItem(xmb.GetAttributeID("directory")).FromUTF8());
|
|
if (!directoryAttr.empty())
|
|
{
|
|
if (VfsPath(directoryAttr).IsDirectory())
|
|
{
|
|
VfsPaths pathnames;
|
|
vfs::GetPathnames(g_VFS, directoryAttr, L"*.js", pathnames);
|
|
for (const VfsPath& path : pathnames)
|
|
{
|
|
// Only load new files (so when the insert succeeds)
|
|
if (Paths.insert(path).second)
|
|
m_ScriptInterface->LoadGlobalScriptFile(path);
|
|
}
|
|
}
|
|
else
|
|
LOGERROR("GUI: Script path %s is not a directory path", directoryAttr.ToUTF8().c_str());
|
|
}
|
|
|
|
CStr code(element.GetText());
|
|
if (!code.empty())
|
|
m_ScriptInterface->LoadGlobalScript(L"Some XML file", code);
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadSprite(const XMBData& xmb, XMBElement element)
|
|
{
|
|
auto sprite = std::make_unique<CGUISprite>();
|
|
|
|
// Get name, we know it exists because of DTD requirements
|
|
CStr name = element.GetAttributes().GetNamedItem(xmb.GetAttributeID("name"));
|
|
|
|
if (m_Sprites.find(name) != m_Sprites.end())
|
|
LOGWARNING("GUI sprite name '%s' used more than once; first definition will be discarded", name.c_str());
|
|
|
|
// shared_ptr to link the effect to every image, faster than copy.
|
|
std::shared_ptr<SGUIImageEffects> effects;
|
|
|
|
for (XMBElement child : element.GetChildNodes())
|
|
{
|
|
std::string_view ElementName(xmb.GetElementStringView(child.GetNodeName()));
|
|
if (ElementName == "image")
|
|
Xeromyces_ReadImage(xmb, child, *sprite);
|
|
else if (ElementName == "effect")
|
|
{
|
|
if (effects)
|
|
LOGERROR("GUI <sprite> must not have more than one <effect>");
|
|
else
|
|
{
|
|
effects = std::make_shared<SGUIImageEffects>();
|
|
Xeromyces_ReadEffects(xmb, child, *effects);
|
|
}
|
|
}
|
|
else
|
|
debug_warn(L"Invalid data - DTD shouldn't allow this");
|
|
}
|
|
|
|
// Apply the effects to every image (unless the image overrides it with
|
|
// different effects)
|
|
if (effects)
|
|
{
|
|
for (const std::unique_ptr<SGUIImage>& image : sprite->m_Images)
|
|
if (!image->m_Effects)
|
|
image->m_Effects = effects;
|
|
}
|
|
|
|
m_Sprites.erase(name);
|
|
m_Sprites.emplace(name, std::move(sprite));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadImage(const XMBData& xmb, XMBElement element, CGUISprite& parent)
|
|
{
|
|
auto image = std::make_unique<SGUIImage>();
|
|
|
|
// TODO Gee: Setup defaults here (or maybe they are in the SGUIImage ctor)
|
|
|
|
for (XMBAttribute attr : element.GetAttributes())
|
|
{
|
|
std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
|
|
CStrW attr_value(attr.Value.FromUTF8());
|
|
|
|
if (attr_name == "texture")
|
|
{
|
|
image->m_TextureName = VfsPath("art/textures/ui") / attr_value;
|
|
}
|
|
else if (attr_name == "size")
|
|
{
|
|
image->m_Size.FromString(attr.Value);
|
|
}
|
|
else if (attr_name == "texture_size")
|
|
{
|
|
image->m_TextureSize.FromString(attr.Value);
|
|
}
|
|
else if (attr_name == "real_texture_placement")
|
|
{
|
|
CRect rect;
|
|
if (!ParseString<CRect>(this, attr_value, rect))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
|
|
else
|
|
image->m_TexturePlacementInFile = rect;
|
|
}
|
|
else if (attr_name == "fixed_h_aspect_ratio")
|
|
{
|
|
float val;
|
|
if (!ParseString<float>(this, attr_value, val))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
|
|
else
|
|
image->m_FixedHAspectRatio = val;
|
|
}
|
|
else if (attr_name == "round_coordinates")
|
|
{
|
|
bool b;
|
|
if (!ParseString<bool>(this, attr_value, b))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
|
|
else
|
|
image->m_RoundCoordinates = b;
|
|
}
|
|
else if (attr_name == "wrap_mode")
|
|
{
|
|
if (attr_value == L"repeat")
|
|
image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::REPEAT;
|
|
else if (attr_value == L"mirrored_repeat")
|
|
image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::MIRRORED_REPEAT;
|
|
else if (attr_value == L"clamp_to_edge")
|
|
image->m_AddressMode = Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE;
|
|
else
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
|
|
}
|
|
else if (attr_name == "backcolor")
|
|
{
|
|
if (!ParseString<CGUIColor>(this, attr_value, image->m_BackColor))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, utf8_from_wstring(attr_value));
|
|
}
|
|
else
|
|
debug_warn(L"Invalid data - DTD shouldn't allow this");
|
|
}
|
|
|
|
// Look for effects
|
|
for (XMBElement child : element.GetChildNodes())
|
|
{
|
|
std::string_view ElementName(xmb.GetElementStringView(child.GetNodeName()));
|
|
if (ElementName == "effect")
|
|
{
|
|
if (image->m_Effects)
|
|
LOGERROR("GUI <image> must not have more than one <effect>");
|
|
else
|
|
{
|
|
image->m_Effects = std::make_shared<SGUIImageEffects>();
|
|
Xeromyces_ReadEffects(xmb, child, *image->m_Effects);
|
|
}
|
|
}
|
|
else
|
|
debug_warn(L"Invalid data - DTD shouldn't allow this");
|
|
}
|
|
|
|
parent.AddImage(std::move(image));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadEffects(const XMBData& xmb, XMBElement element, SGUIImageEffects& effects)
|
|
{
|
|
for (XMBAttribute attr : element.GetAttributes())
|
|
{
|
|
std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
|
|
if (attr_name == "add_color")
|
|
{
|
|
if (!effects.m_AddColor.ParseString(*this, attr.Value, 0))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr.Value);
|
|
}
|
|
else if (attr_name == "grayscale")
|
|
effects.m_Greyscale = true;
|
|
else
|
|
debug_warn(L"Invalid data - DTD shouldn't allow this");
|
|
}
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadStyle(const XMBData& xmb, XMBElement element)
|
|
{
|
|
SGUIStyle style;
|
|
CStr name;
|
|
|
|
for (XMBAttribute attr : element.GetAttributes())
|
|
{
|
|
std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
|
|
// The "name" setting is actually the name of the style
|
|
// and not a new default
|
|
if (attr_name == "name")
|
|
name = attr.Value;
|
|
else
|
|
style.m_SettingsDefaults.emplace(std::string(attr_name), attr.Value.FromUTF8());
|
|
}
|
|
|
|
m_Styles.erase(name);
|
|
m_Styles.emplace(name, std::move(style));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadScrollBarStyle(const XMBData& xmb, XMBElement element)
|
|
{
|
|
SGUIScrollBarStyle scrollbar;
|
|
CStr name;
|
|
|
|
// Setup some defaults.
|
|
scrollbar.m_MinimumBarSize = 0.f;
|
|
// Using 1.0e10 as a substitute for infinity
|
|
scrollbar.m_MaximumBarSize = 1.0e10;
|
|
scrollbar.m_UseEdgeButtons = false;
|
|
|
|
for (XMBAttribute attr : element.GetAttributes())
|
|
{
|
|
std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
|
|
CStr attr_value(attr.Value);
|
|
|
|
if (attr_value == "null")
|
|
continue;
|
|
|
|
if (attr_name == "name")
|
|
name = attr_value;
|
|
else if (attr_name == "show_edge_buttons")
|
|
{
|
|
bool b;
|
|
if (!ParseString<bool>(this, attr_value.FromUTF8(), b))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
|
|
else
|
|
scrollbar.m_UseEdgeButtons = b;
|
|
}
|
|
else if (attr_name == "width")
|
|
{
|
|
float f;
|
|
if (!ParseString<float>(this, attr_value.FromUTF8(), f))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
|
|
else
|
|
scrollbar.m_Width = f;
|
|
}
|
|
else if (attr_name == "minimum_bar_size")
|
|
{
|
|
float f;
|
|
if (!ParseString<float>(this, attr_value.FromUTF8(), f))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
|
|
else
|
|
scrollbar.m_MinimumBarSize = f;
|
|
}
|
|
else if (attr_name == "maximum_bar_size")
|
|
{
|
|
float f;
|
|
if (!ParseString<float>(this, attr_value.FromUTF8(), f))
|
|
LOGERROR("GUI: Error parsing '%s' (\"%s\")", attr_name, attr_value);
|
|
else
|
|
scrollbar.m_MaximumBarSize = f;
|
|
}
|
|
else if (attr_name == "sprite_button_top")
|
|
scrollbar.m_SpriteButtonTop = attr_value;
|
|
else if (attr_name == "sprite_button_top_pressed")
|
|
scrollbar.m_SpriteButtonTopPressed = attr_value;
|
|
else if (attr_name == "sprite_button_top_disabled")
|
|
scrollbar.m_SpriteButtonTopDisabled = attr_value;
|
|
else if (attr_name == "sprite_button_top_over")
|
|
scrollbar.m_SpriteButtonTopOver = attr_value;
|
|
else if (attr_name == "sprite_button_bottom")
|
|
scrollbar.m_SpriteButtonBottom = attr_value;
|
|
else if (attr_name == "sprite_button_bottom_pressed")
|
|
scrollbar.m_SpriteButtonBottomPressed = attr_value;
|
|
else if (attr_name == "sprite_button_bottom_disabled")
|
|
scrollbar.m_SpriteButtonBottomDisabled = attr_value;
|
|
else if (attr_name == "sprite_button_bottom_over")
|
|
scrollbar.m_SpriteButtonBottomOver = attr_value;
|
|
else if (attr_name == "sprite_back_vertical")
|
|
scrollbar.m_SpriteBackVertical = attr_value;
|
|
else if (attr_name == "sprite_slider_vertical")
|
|
scrollbar.m_SpriteSliderVertical = attr_value;
|
|
else if (attr_name == "sprite_slider_vertical_over")
|
|
scrollbar.m_SpriteSliderVerticalOver = attr_value;
|
|
else if (attr_name == "sprite_slider_vertical_pressed")
|
|
scrollbar.m_SpriteSliderVerticalPressed = attr_value;
|
|
else if (attr_name == "sprite_back_horizontal")
|
|
scrollbar.m_SpriteBackHorizontal = attr_value;
|
|
else if (attr_name == "sprite_slider_horizontal")
|
|
scrollbar.m_SpriteSliderHorizontal = attr_value;
|
|
else if (attr_name == "sprite_slider_horizontal_over")
|
|
scrollbar.m_SpriteSliderHorizontalOver = attr_value;
|
|
else if (attr_name == "sprite_slider_horizontal_pressed")
|
|
scrollbar.m_SpriteSliderHorizontalPressed = attr_value;
|
|
}
|
|
|
|
m_ScrollBarStyles.erase(name);
|
|
m_ScrollBarStyles.emplace(name, std::move(scrollbar));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadIcon(const XMBData& xmb, XMBElement element)
|
|
{
|
|
SGUIIcon icon;
|
|
CStr name;
|
|
|
|
for (XMBAttribute attr : element.GetAttributes())
|
|
{
|
|
std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
|
|
CStr attr_value(attr.Value);
|
|
|
|
if (attr_value == "null")
|
|
continue;
|
|
|
|
if (attr_name == "name")
|
|
name = attr_value;
|
|
else if (attr_name == "sprite")
|
|
icon.m_SpriteName = attr_value;
|
|
else if (attr_name == "size")
|
|
{
|
|
CSize2D size;
|
|
if (!ParseString<CSize2D>(this, attr_value.FromUTF8(), size))
|
|
LOGERROR("Error parsing '%s' (\"%s\") inside <icon>.", attr_name, attr_value);
|
|
else
|
|
icon.m_Size = size;
|
|
}
|
|
else
|
|
debug_warn(L"Invalid data - DTD shouldn't allow this");
|
|
}
|
|
|
|
m_Icons.erase(name);
|
|
m_Icons.emplace(name, std::move(icon));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadTooltip(const XMBData& xmb, XMBElement element)
|
|
{
|
|
std::unique_ptr<IGUIObject> object{std::make_unique<CTooltip>(*this)};
|
|
|
|
for (XMBAttribute attr : element.GetAttributes())
|
|
{
|
|
std::string_view attr_name(xmb.GetAttributeStringView(attr.Name));
|
|
CStr attr_value(attr.Value);
|
|
|
|
if (attr_name == "name")
|
|
object->SetName("__tooltip_" + attr_value);
|
|
else
|
|
object->SetSettingFromString(std::string(attr_name), attr_value.FromUTF8(), true);
|
|
}
|
|
|
|
AddObject(*m_BaseObject, std::move(object));
|
|
}
|
|
|
|
void CGUI::Xeromyces_ReadColor(const XMBData& xmb, XMBElement element)
|
|
{
|
|
XMBAttributeList attributes = element.GetAttributes();
|
|
CStr name = attributes.GetNamedItem(xmb.GetAttributeID("name"));
|
|
|
|
// Try parsing value
|
|
CStr value(element.GetText());
|
|
if (value.empty())
|
|
return;
|
|
|
|
CColor color;
|
|
if (color.ParseString(value))
|
|
{
|
|
m_PreDefinedColors.erase(name);
|
|
m_PreDefinedColors.emplace(
|
|
std::piecewise_construct,
|
|
std::forward_as_tuple(name),
|
|
std::forward_as_tuple(color.r, color.g, color.b, color.a));
|
|
}
|
|
else
|
|
LOGERROR("GUI: Unable to create custom color '%s'. Invalid color syntax.", name.c_str());
|
|
}
|
|
|
|
CGUI::ModuleArtifact::ModuleArtifact(const ScriptRequest& rq, VfsPath filename):
|
|
result{rq, std::move(filename)},
|
|
moduleNamespace{rq.cx}
|
|
{}
|