0ad/source/gui/ObjectBases/IGUIObject.cpp

575 lines
16 KiB
C++
Raw Normal View History

Allow setting CGUISize properties directly Previously, modifying individual CGUISize properties (left, top, right, bottom) via JavaScript had no effect unless the entire size object was copied, modified, and reassigned—resulting in verbose and error-prone code. This update introduces proper JavaScript property accessors, enabling direct reading and writing of CGUISize members. The result is a more intuitive and scriptable API. Key Changes: - Implemented JavaScript get/set accessors for each CGUISize property, directly interacting with the native object. - Retained support for string assignments to CGUISize - Maintained the GUISize JS object for backward compatibility, also marked as deprecated. - Improved CGUISimpleSetting<CGUISize> to allow partial updates via direct property access. - allow assigning size from object with pixel and percent values You can now assign size using an object with properties like: size = { left: number, right: number, top: number, bottom: number, rleft: number, rright: number, rtop: number, rbottom: number } - One or more properties can be specified, the missing properties with fill as zero. - Assignment triggers an immediate update event. Delayed Setting Notifications: - Introduced a mechanism for delayed setting notifications, currently only used in CGUISimpleSetting<CGUISize>, to avoid redundant recalculations. - Ensured that getComputedSize triggers any pending delayed notifications before returning the size. This change significantly enhances the developer experience and brings the behavior in line with typical expectations for direct property manipulation in JavaScript.
2025-06-04 14:22:34 -07:00
/* Copyright (C) 2025 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 "IGUIObject.h"
#include "gui/CGUI.h"
#include "gui/CGUISetting.h"
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/Scripting/JSInterface_GUIProxy.h"
#include "js/Conversions.h"
#include "ps/CLogger.h"
#include "ps/Profile.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptExtraHeaders.h"
#include "scriptinterface/ScriptConversions.h"
#include "soundmanager/ISoundManager.h"
#include <algorithm>
#include <string_view>
#include <unordered_map>
const CStr IGUIObject::EventNameMouseEnter = "MouseEnter";
const CStr IGUIObject::EventNameMouseMove = "MouseMove";
const CStr IGUIObject::EventNameMouseLeave = "MouseLeave";
IGUIObject::IGUIObject(CGUI& pGUI)
: m_pGUI(pGUI),
m_pParent(),
m_MouseHovering(),
m_LastClickTime(),
m_Enabled(this, "enabled", true),
m_Hidden(this, "hidden", false),
m_Size(this, "size"),
m_Style(this, "style"),
m_Hotkey(this, "hotkey"),
m_Z(this, "z"),
m_Absolute(this, "absolute", true),
m_Ghost(this, "ghost", false),
m_AspectRatio(this, "aspectratio"),
m_Tooltip(this, "tooltip"),
m_TooltipStyle(this, "tooltip_style")
{
2003-11-23 18:18:41 -08:00
}
IGUIObject::~IGUIObject()
{
if (!m_ScriptHandlers.empty())
JS_RemoveExtraGCRootsTracer(ScriptRequest(m_pGUI.GetScriptInterface()).cx, Trace, this);
2003-11-23 18:18:41 -08:00
// m_Children is deleted along all other GUI Objects in the CGUI destructor
2003-11-23 18:18:41 -08:00
}
void IGUIObject::RegisterChild(IGUIObject* child)
2003-11-23 18:18:41 -08:00
{
child->SetParent(this);
m_Children.push_back(child);
}
void IGUIObject::UnregisterChild(IGUIObject* child)
{
std::vector<IGUIObject*>::iterator it = std::find(m_Children.begin(), m_Children.end(), child);
if (it != m_Children.end())
{
(*it)->m_pParent = nullptr;
m_Children.erase(it);
}
2003-11-23 18:18:41 -08:00
}
void IGUIObject::RegisterSetting(const CStr& Name, IGUISetting* setting)
2003-11-23 18:18:41 -08:00
{
if (SettingExists(Name))
LOGERROR("The setting '%s' already exists on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
else
m_Settings.emplace(Name, setting);
}
void IGUIObject::ReregisterSetting(const CStr& Name, IGUISetting* setting)
{
if (!SettingExists(Name))
LOGERROR("The setting '%s' must already exist on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
else
m_Settings.at(Name) = setting;
}
bool IGUIObject::SettingExists(const CStr& Setting) const
{
return m_Settings.find(Setting) != m_Settings.end();
}
bool IGUIObject::SetSettingFromString(const CStr& Setting, const CStrW& Value, const bool SendMessage)
{
const std::map<CStr, IGUISetting*>::iterator it = m_Settings.find(Setting);
if (it == m_Settings.end())
{
LOGERROR("GUI object '%s' has no property called '%s', can't set parse and set value '%s'", GetPresentableName().c_str(), Setting.c_str(), Value.ToUTF8().c_str());
return false;
}
return it->second->FromString(Value, SendMessage);
}
void IGUIObject::SettingChanged(const CStr& Setting, const bool SendMessage)
{
if (Setting == "size")
{
// If setting was "size", we need to re-cache itself and all children
RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
}
else if (Setting == "hidden")
{
// Hiding an object requires us to reset it and all children
if (m_Hidden)
RecurseObject(nullptr, &IGUIObject::ResetStates);
}
else if (Setting == "style")
m_pGUI.SetObjectStyle(this, m_Style);
if (SendMessage)
{
SGUIMessage msg(GUIM_SETTINGS_UPDATED, Setting);
HandleMessage(msg);
// inform to parents until get to the root object
// for now only size and hidden settings are bubbled up
if (GetParent() && (Setting == "size" || Setting == "hidden"))
{
EGUIMessageType type = Setting == "size" ? GUIM_CHILD_RESIZED : GUIM_CHILD_TOGGLE_VISIBILITY;
SGUIMessage msg(type, GetName());
msg.Skip(true);
IGUIObject* parent = GetParent();
while (parent)
{
parent->HandleMessage(msg);
if (!msg.skipped)
break;
parent = parent->GetParent();
}
}
}
}
bool IGUIObject::IsMouseOver() const
2003-11-23 18:18:41 -08:00
{
if (m_VisibleArea)
return m_VisibleArea.PointInside(m_pGUI.GetMousePos());
return m_CachedActualSize.PointInside(m_pGUI.GetMousePos());
2003-11-23 18:18:41 -08:00
}
void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver)
2003-11-23 18:18:41 -08:00
{
if (pMouseOver == this)
{
if (!m_MouseHovering)
SendMouseEvent(GUIM_MOUSE_ENTER,EventNameMouseEnter);
2003-11-23 18:18:41 -08:00
m_MouseHovering = true;
SendMouseEvent(GUIM_MOUSE_OVER, EventNameMouseMove);
2003-11-23 18:18:41 -08:00
}
else
2003-11-23 18:18:41 -08:00
{
if (m_MouseHovering)
{
m_MouseHovering = false;
SendMouseEvent(GUIM_MOUSE_LEAVE, EventNameMouseLeave);
2003-11-23 18:18:41 -08:00
}
}
}
void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject)
2003-11-23 18:18:41 -08:00
{
if (!IsMouseOver())
return;
// Check if we've got competition at all
if (pObject == nullptr)
2003-11-23 18:18:41 -08:00
{
pObject = this;
return;
}
2003-11-23 18:18:41 -08:00
// Or if it's closer
if (GetBufferedZ() >= pObject->GetBufferedZ())
{
pObject = this;
return;
2003-11-23 18:18:41 -08:00
}
}
IGUIObject* IGUIObject::GetParent() const
2003-11-23 18:18:41 -08:00
{
// Important, we're not using GetParent() for these
// checks, that could screw it up
if (m_pParent && m_pParent->m_pParent == nullptr)
return nullptr;
2003-11-23 18:18:41 -08:00
return m_pParent;
}
void IGUIObject::ResetStates()
{
// Notify the gui that we aren't hovered anymore
UpdateMouseOver(nullptr);
}
2003-11-24 09:13:37 -08:00
void IGUIObject::UpdateCachedSize()
{
2003-11-24 18:47:12 -08:00
// If absolute="false" and the object has got a parent,
// use its cached size instead of the screen. Notice
// it must have just been cached for it to work.
if (!m_Absolute && m_pParent && !IsRootObject())
m_CachedActualSize = m_Size->GetSize(m_pParent->m_CachedActualSize);
2003-11-24 18:47:12 -08:00
else
m_CachedActualSize = m_Size->GetSize(CRect(m_pGUI.GetWindowSize()));
2004-05-28 21:06:50 -07:00
// In a few cases, GUI objects have to resize to fill the screen
// but maintain a constant aspect ratio.
// Adjust the size to be the max possible, centered in the original size:
if (m_AspectRatio)
{
if (m_CachedActualSize.GetWidth() > m_CachedActualSize.GetHeight() * m_AspectRatio)
{
float delta = m_CachedActualSize.GetWidth() - m_CachedActualSize.GetHeight() * m_AspectRatio;
m_CachedActualSize.left += delta/2.f;
m_CachedActualSize.right -= delta/2.f;
}
else
{
float delta = m_CachedActualSize.GetHeight() - m_CachedActualSize.GetWidth() / m_AspectRatio;
m_CachedActualSize.bottom -= delta/2.f;
m_CachedActualSize.top += delta/2.f;
}
}
2003-11-24 09:13:37 -08:00
}
CRect IGUIObject::GetComputedSize()
{
Allow setting CGUISize properties directly Previously, modifying individual CGUISize properties (left, top, right, bottom) via JavaScript had no effect unless the entire size object was copied, modified, and reassigned—resulting in verbose and error-prone code. This update introduces proper JavaScript property accessors, enabling direct reading and writing of CGUISize members. The result is a more intuitive and scriptable API. Key Changes: - Implemented JavaScript get/set accessors for each CGUISize property, directly interacting with the native object. - Retained support for string assignments to CGUISize - Maintained the GUISize JS object for backward compatibility, also marked as deprecated. - Improved CGUISimpleSetting<CGUISize> to allow partial updates via direct property access. - allow assigning size from object with pixel and percent values You can now assign size using an object with properties like: size = { left: number, right: number, top: number, bottom: number, rleft: number, rright: number, rtop: number, rbottom: number } - One or more properties can be specified, the missing properties with fill as zero. - Assignment triggers an immediate update event. Delayed Setting Notifications: - Introduced a mechanism for delayed setting notifications, currently only used in CGUISimpleSetting<CGUISize>, to avoid redundant recalculations. - Ensured that getComputedSize triggers any pending delayed notifications before returning the size. This change significantly enhances the developer experience and brings the behavior in line with typical expectations for direct property manipulation in JavaScript.
2025-06-04 14:22:34 -07:00
// Ensure the size is up to date before we use it.
m_Settings.at("size")->DispatchDelayedSettingChange();
UpdateCachedSize();
return m_CachedActualSize;
}
bool IGUIObject::ApplyStyle(const CStr& StyleName)
{
if (!m_pGUI.HasStyle(StyleName))
{
LOGERROR("IGUIObject: Trying to use style '%s' that doesn't exist.", StyleName.c_str());
return false;
}
// The default style may specify settings for any GUI object.
// Other styles are reported if they specify a Setting that does not exist,
// so that the XML author is informed and can correct the style.
for (const std::pair<const CStr, CStrW>& p : m_pGUI.GetStyle(StyleName).m_SettingsDefaults)
{
if (SettingExists(p.first))
m_Settings.at(p.first)->FromString(p.second, true);
else if (StyleName != "default")
LOGWARNING("GUI object has no setting \"%s\", but the style \"%s\" defines it", p.first, StyleName.c_str());
}
return true;
}
float IGUIObject::GetBufferedZ() const
{
if (m_Absolute)
return m_Z;
2004-05-28 21:06:50 -07:00
if (GetParent())
return GetParent()->GetBufferedZ() + m_Z;
// In philosophy, a parentless object shouldn't be able to have a relative sizing,
// but we'll accept it so that absolute can be used as default without a complaint.
// Also, you could consider those objects children to the screen resolution.
return m_Z;
}
void IGUIObject::RegisterScriptHandler(const CStr& eventName, const CStr& Code, CGUI& pGUI)
{
ScriptRequest rq(pGUI.GetScriptInterface());
const int paramCount = 1;
const char* paramNames[paramCount] = { "mouse" };
// Location to report errors from
CStr CodeName = GetName() + " " + eventName;
// Generate a unique name
static int x = 0;
char buf[64];
sprintf_s(buf, ARRAY_SIZE(buf), "__eventhandler%d (%s)", x++, eventName.c_str());
// TODO: this is essentially the same code as ScriptInterface::LoadScript (with a tweak for the argument).
JS::CompileOptions options(rq.cx);
options.setFileAndLine(CodeName.c_str(), 0);
options.setIsRunOnce(false);
options.setForceStrictMode();
JS::SourceText<mozilla::Utf8Unit> src;
ENSURE(src.init(rq.cx, Code.c_str(), Code.length(), JS::SourceOwnership::Borrowed));
JS::RootedObjectVector emptyScopeChain(rq.cx);
JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, buf, paramCount, paramNames, src));
if (func == nullptr)
{
LOGERROR("RegisterScriptHandler: Failed to compile the script for %s", eventName.c_str());
return;
}
JS::RootedObject funcObj(rq.cx, JS_GetFunctionObject(func));
SetScriptHandler(eventName, funcObj);
}
void IGUIObject::SetScriptHandler(const CStr& eventName, JS::HandleObject Function)
{
if (m_ScriptHandlers.empty())
JS_AddExtraGCRootsTracer(ScriptRequest(m_pGUI.GetScriptInterface()).cx, Trace, this);
m_ScriptHandlers[eventName] = JS::Heap<JSObject*>(Function);
if (std::find(m_pGUI.m_EventObjects[eventName].begin(), m_pGUI.m_EventObjects[eventName].end(), this) == m_pGUI.m_EventObjects[eventName].end())
m_pGUI.m_EventObjects[eventName].emplace_back(this);
}
void IGUIObject::UnsetScriptHandler(const CStr& eventName)
{
std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
if (it == m_ScriptHandlers.end())
return;
m_ScriptHandlers.erase(it);
if (m_ScriptHandlers.empty())
JS_RemoveExtraGCRootsTracer(ScriptRequest(m_pGUI.GetScriptInterface()).cx, Trace, this);
std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it2 = m_pGUI.m_EventObjects.find(eventName);
if (it2 == m_pGUI.m_EventObjects.end())
return;
std::vector<IGUIObject*>& handlers = it2->second;
handlers.erase(std::remove(handlers.begin(), handlers.end(), this), handlers.end());
if (handlers.empty())
m_pGUI.m_EventObjects.erase(it2);
}
InReaction IGUIObject::SendEvent(EGUIMessageType type, const CStr& eventName)
{
PROFILE2_EVENT("gui event");
PROFILE2_ATTR("type: %s", eventName.c_str());
PROFILE2_ATTR("object: %s", m_Name.c_str());
SGUIMessage msg(type);
HandleMessage(msg);
ScriptEvent(eventName);
return msg.skipped ? IN_PASS : IN_HANDLED;
}
InReaction IGUIObject::SendMouseEvent(EGUIMessageType type, const CStr& eventName)
{
PROFILE2_EVENT("gui mouse event");
PROFILE2_ATTR("type: %s", eventName.c_str());
PROFILE2_ATTR("object: %s", m_Name.c_str());
SGUIMessage msg(type);
if (type == GUIM_MOUSE_WHEEL_UP || type == GUIM_MOUSE_WHEEL_DOWN || type == GUIM_MOUSE_WHEEL_LEFT || type == GUIM_MOUSE_WHEEL_RIGHT)
msg.Skip();
HandleMessage(msg);
ScriptRequest rq(m_pGUI.GetScriptInterface());
// Set up the 'mouse' parameter
JS::RootedValue mouse(rq.cx);
const CVector2D& mousePos = m_pGUI.GetMousePos();
std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
if (it != m_ScriptHandlers.end())
{
Script::CreateObject(
rq,
&mouse,
"x", mousePos.X,
"y", mousePos.Y,
"buttons", m_pGUI.GetMouseButtons());
JS::RootedValueVector paramData(rq.cx);
ignore_result(paramData.append(mouse));
ScriptEvent(eventName, paramData);
if (type == GUIM_MOUSE_WHEEL_UP || type == GUIM_MOUSE_WHEEL_DOWN || type == GUIM_MOUSE_WHEEL_LEFT || type == GUIM_MOUSE_WHEEL_RIGHT)
msg.Skip(false);
}
// inform to parents until get to the root object
// for now only wheel events are inform to parents
if ((type == GUIM_MOUSE_WHEEL_UP || type == GUIM_MOUSE_WHEEL_DOWN || type == GUIM_MOUSE_WHEEL_LEFT || type == GUIM_MOUSE_WHEEL_RIGHT) && msg.skipped)
{
if (GetParent())
msg.Skip(GetParent()->SendMouseEvent(type, eventName) == IN_PASS);
else
msg.Skip(false);
}
return msg.skipped ? IN_PASS : IN_HANDLED;
}
void IGUIObject::ScriptEvent(const CStr& eventName)
{
ScriptEventWithReturn(eventName);
}
bool IGUIObject::ScriptEventWithReturn(const CStr& eventName)
{
if (m_ScriptHandlers.find(eventName) == m_ScriptHandlers.end())
return false;
ScriptRequest rq(m_pGUI.GetScriptInterface());
JS::RootedValueVector paramData(rq.cx);
return ScriptEventWithReturn(eventName, paramData);
}
void IGUIObject::ScriptEvent(const CStr& eventName, const JS::HandleValueArray& paramData)
{
ScriptEventWithReturn(eventName, paramData);
}
bool IGUIObject::ScriptEventWithReturn(const CStr& eventName, const JS::HandleValueArray& paramData)
{
std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
if (it == m_ScriptHandlers.end())
return false;
ScriptRequest rq(m_pGUI.GetScriptInterface());
JS::RootedObject obj(rq.cx, GetJSObject());
JS::RootedValue handlerVal(rq.cx, JS::ObjectValue(*it->second));
JS::RootedValue result(rq.cx);
if (!JS_CallFunctionValue(rq.cx, obj, handlerVal, paramData, &result))
{
LOGERROR("Errors executing script event \"%s\"", eventName.c_str());
ScriptException::CatchPending(rq);
return false;
}
return JS::ToBoolean(result);
}
JSObject* IGUIObject::GetJSObject()
{
// Cache the object when somebody first asks for it, because otherwise
// we end up doing far too much object allocation.
if (!m_JSObject)
CreateJSObject();
return m_JSObject->Get();
}
bool IGUIObject::IsEnabled() const
{
return m_Enabled;
}
bool IGUIObject::IsHidden() const
{
return m_Hidden;
}
bool IGUIObject::IsHiddenOrGhost() const
{
return m_Hidden || m_Ghost;
}
void IGUIObject::PlaySound(const CStrW& soundPath) const
{
if (g_SoundManager && !soundPath.empty())
g_SoundManager->PlayAsUI(soundPath.c_str(), false);
}
CStr IGUIObject::GetPresentableName() const
{
// __internal(), must be at least 13 letters to be able to be
// an internal name
if (m_Name.length() <= 12)
return m_Name;
if (std::string_view{m_Name}.substr(0, 10) == "__internal")
return CStr("[unnamed object]");
else
return m_Name;
}
void IGUIObject::SetFocus()
{
m_pGUI.SetFocusedObject(this);
}
void IGUIObject::ReleaseFocus()
{
m_pGUI.SetFocusedObject(nullptr);
}
bool IGUIObject::IsFocused() const
{
return m_pGUI.GetFocusedObject() == this;
}
bool IGUIObject::IsBaseObject() const
{
return this == m_pGUI.GetBaseObject();
}
bool IGUIObject::IsRootObject() const
{
return m_pParent == m_pGUI.GetBaseObject();
}
void IGUIObject::TraceMember(JSTracer* trc)
{
// Please ensure to adapt the Tracer enabling and disabling in accordance with the GC things traced!
for (std::pair<const CStr, JS::Heap<JSObject*>>& handler : m_ScriptHandlers)
JS::TraceEdge(trc, &handler.second, "IGUIObject::m_ScriptHandlers");
}
void IGUIObject::DrawInArea(CCanvas2D& canvas, CRect& area)
{
bool isInsideBoundaries = false;
RecurseObject(nullptr, &IGUIObject::SetIsInsideBoundaries, isInsideBoundaries);
if (!area.IntersectWith(m_CachedActualSize))
return;
CRect intersection = area.Intersection(m_CachedActualSize);
m_VisibleArea = intersection;
Draw(canvas);
isInsideBoundaries = true;
RecurseObject(nullptr, &IGUIObject::SetIsInsideBoundaries, isInsideBoundaries);
}
bool IGUIObject::IsHiddenOrGhostOrOutOfBoundaries() const {
return !m_IsInsideBoundaries || IsHiddenOrGhost();
}
Allow setting CGUISize properties directly Previously, modifying individual CGUISize properties (left, top, right, bottom) via JavaScript had no effect unless the entire size object was copied, modified, and reassigned—resulting in verbose and error-prone code. This update introduces proper JavaScript property accessors, enabling direct reading and writing of CGUISize members. The result is a more intuitive and scriptable API. Key Changes: - Implemented JavaScript get/set accessors for each CGUISize property, directly interacting with the native object. - Retained support for string assignments to CGUISize - Maintained the GUISize JS object for backward compatibility, also marked as deprecated. - Improved CGUISimpleSetting<CGUISize> to allow partial updates via direct property access. - allow assigning size from object with pixel and percent values You can now assign size using an object with properties like: size = { left: number, right: number, top: number, bottom: number, rleft: number, rright: number, rtop: number, rbottom: number } - One or more properties can be specified, the missing properties with fill as zero. - Assignment triggers an immediate update event. Delayed Setting Notifications: - Introduced a mechanism for delayed setting notifications, currently only used in CGUISimpleSetting<CGUISize>, to avoid redundant recalculations. - Ensured that getComputedSize triggers any pending delayed notifications before returning the size. This change significantly enhances the developer experience and brings the behavior in line with typical expectations for direct property manipulation in JavaScript.
2025-06-04 14:22:34 -07:00
void IGUIObject::DispatchDelayedSettingChanges()
{
for (const auto& setting : m_Settings)
setting.second->DispatchDelayedSettingChange();
}