/* Copyright (C) 2015 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 . */ #include "precompiled.h" #include "GUI.h" #include "gui/scripting/JSInterface_GUITypes.h" #include "gui/scripting/JSInterface_IGUIObject.h" #include "ps/CLogger.h" #include "scriptinterface/ScriptInterface.h" IGUIObject::IGUIObject() : m_pGUI(NULL), m_pParent(NULL), m_MouseHovering(false), m_LastClickTime() { AddSetting(GUIST_bool, "enabled"); AddSetting(GUIST_bool, "hidden"); AddSetting(GUIST_CClientArea, "size"); AddSetting(GUIST_CStr, "style"); AddSetting(GUIST_CStr, "hotkey"); AddSetting(GUIST_float, "z"); AddSetting(GUIST_bool, "absolute"); AddSetting(GUIST_bool, "ghost"); AddSetting(GUIST_float, "aspectratio"); AddSetting(GUIST_CStrW, "tooltip"); AddSetting(GUIST_CStr, "tooltip_style"); // Setup important defaults GUI::SetSetting(this, "hidden", false); GUI::SetSetting(this, "ghost", false); GUI::SetSetting(this, "enabled", true); GUI::SetSetting(this, "absolute", true); } IGUIObject::~IGUIObject() { for (const std::pair& p : m_Settings) switch (p.second.m_Type) { // delete() needs to know the type of the variable - never delete a void* #define TYPE(t) case GUIST_##t: delete (t*)p.second.m_pSetting; break; #include "GUItypes.h" #undef TYPE default: debug_warn(L"Invalid setting type"); } if (m_pGUI) JS_RemoveExtraGCRootsTracer(m_pGUI->GetScriptInterface()->GetJSRuntime(), Trace, this); } //------------------------------------------------------------------- // Functions //------------------------------------------------------------------- void IGUIObject::SetGUI(CGUI* const& pGUI) { if (!m_pGUI) JS_AddExtraGCRootsTracer(pGUI->GetScriptInterface()->GetJSRuntime(), Trace, this); m_pGUI = pGUI; } void IGUIObject::AddChild(IGUIObject* pChild) { // ENSURE(pChild); pChild->SetParent(this); m_Children.push_back(pChild); // If this (not the child) object is already attached // to a CGUI, it pGUI pointer will be non-null. // This will mean we'll have to check if we're using // names already used. if (pChild->GetGUI()) { try { // Atomic function, if it fails it won't // have changed anything //UpdateObjects(); pChild->GetGUI()->UpdateObjects(); } catch (PSERROR_GUI&) { // If anything went wrong, reverse what we did and throw // an exception telling it never added a child m_Children.erase(m_Children.end()-1); throw; } } // else do nothing } void IGUIObject::AddToPointersMap(map_pObjects& ObjectMap) { // Just don't do anything about the top node if (m_pParent == NULL) return; // Now actually add this one // notice we won't add it if it's doesn't have any parent // (i.e. being the base object) if (m_Name.empty()) { throw PSERROR_GUI_ObjectNeedsName(); } if (ObjectMap.count(m_Name) > 0) { throw PSERROR_GUI_NameAmbiguity(m_Name.c_str()); } else { ObjectMap[m_Name] = this; } } void IGUIObject::Destroy() { // Is there anything besides the children to destroy? } void IGUIObject::AddSetting(const EGUISettingType& Type, const CStr& Name) { // Is name already taken? if (m_Settings.count(Name) >= 1) return; // Construct, and set type m_Settings[Name].m_Type = Type; switch (Type) { #define TYPE(type) \ case GUIST_##type: \ m_Settings[Name].m_pSetting = new type(); \ break; // Construct the setting. #include "GUItypes.h" #undef TYPE default: debug_warn(L"IGUIObject::AddSetting failed, type not recognized!"); break; } } bool IGUIObject::MouseOver() { if (!GetGUI()) throw PSERROR_GUI_OperationNeedsGUIObject(); return m_CachedActualSize.PointInside(GetMousePos()); } bool IGUIObject::MouseOverIcon() { return false; } CPos IGUIObject::GetMousePos() const { if (GetGUI()) return GetGUI()->m_MousePos; return CPos(); } void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver) { // Check if this is the object being hovered. if (pMouseOver == this) { if (!m_MouseHovering) { // It wasn't hovering, so that must mean it just entered SendEvent(GUIM_MOUSE_ENTER, "mouseenter"); } // Either way, set to true m_MouseHovering = true; // call mouse over SendEvent(GUIM_MOUSE_OVER, "mousemove"); } else // Some other object (or none) is hovered { if (m_MouseHovering) { m_MouseHovering = false; SendEvent(GUIM_MOUSE_LEAVE, "mouseleave"); } } } bool IGUIObject::SettingExists(const CStr& Setting) const { // Because GetOffsets will direct dynamically defined // classes with polymorphism to respective ms_SettingsInfo // we need to make no further updates on this function // in derived classes. //return (GetSettingsInfo().count(Setting) >= 1); return (m_Settings.count(Setting) >= 1); } PSRETURN IGUIObject::SetSetting(const CStr& Setting, const CStrW& Value, const bool& SkipMessage) { if (!SettingExists(Setting)) return PSRETURN_GUI_InvalidSetting; // Get setting SGUISetting set = m_Settings[Setting]; #define TYPE(type) \ else if (set.m_Type == GUIST_##type) \ { \ type _Value; \ if (!GUI::ParseString(Value, _Value)) \ return PSRETURN_GUI_UnableToParse; \ GUI::SetSetting(this, Setting, _Value, SkipMessage); \ } if (0) ; // else... #include "GUItypes.h" #undef TYPE else { // Why does it always fail? //return PS_FAIL; return LogInvalidSettings(Setting); } return PSRETURN_OK; } PSRETURN IGUIObject::GetSettingType(const CStr& Setting, EGUISettingType& Type) const { if (!SettingExists(Setting)) return LogInvalidSettings(Setting); if (m_Settings.find(Setting) == m_Settings.end()) return LogInvalidSettings(Setting); Type = m_Settings.find(Setting)->second.m_Type; return PSRETURN_OK; } void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject) { if (!MouseOver()) return; // Check if we've got competition at all if (pObject == NULL) { pObject = this; return; } // Or if it's closer if (GetBufferedZ() >= pObject->GetBufferedZ()) { pObject = this; return; } } IGUIObject* IGUIObject::GetParent() const { // Important, we're not using GetParent() for these // checks, that could screw it up if (m_pParent) { if (m_pParent->m_pParent == NULL) return NULL; } return m_pParent; } void IGUIObject::UpdateCachedSize() { bool absolute; GUI::GetSetting(this, "absolute", absolute); float aspectratio = 0.f; GUI::GetSetting(this, "aspectratio", aspectratio); CClientArea ca; GUI::GetSetting(this, "size", ca); // 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 (absolute == false && m_pParent && !IsRootObject()) m_CachedActualSize = ca.GetClientArea(m_pParent->m_CachedActualSize); else m_CachedActualSize = ca.GetClientArea(CRect(0.f, 0.f, g_xres * g_GuiScale, g_yres * g_GuiScale)); // 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 (aspectratio) { if (m_CachedActualSize.GetWidth() > m_CachedActualSize.GetHeight()*aspectratio) { float delta = m_CachedActualSize.GetWidth() - m_CachedActualSize.GetHeight()*aspectratio; m_CachedActualSize.left += delta/2.f; m_CachedActualSize.right -= delta/2.f; } else { float delta = m_CachedActualSize.GetHeight() - m_CachedActualSize.GetWidth()/aspectratio; m_CachedActualSize.bottom -= delta/2.f; m_CachedActualSize.top += delta/2.f; } } } void IGUIObject::LoadStyle(CGUI& GUIinstance, const CStr& StyleName) { // Fetch style if (GUIinstance.m_Styles.count(StyleName) == 1) { LoadStyle(GUIinstance.m_Styles[StyleName]); } else { debug_warn(L"IGUIObject::LoadStyle failed"); } } void IGUIObject::LoadStyle(const SGUIStyle& Style) { // Iterate settings, it won't be able to set them all probably, but that doesn't matter for (const std::pair& p : Style.m_SettingsDefaults) { // Try set setting in object SetSetting(p.first, p.second); // It doesn't matter if it fail, it's not suppose to be able to set every setting. // since it's generic. // The beauty with styles is that it can contain more settings // than exists for the objects using it. So if the SetSetting // fails, don't care. } } float IGUIObject::GetBufferedZ() const { bool absolute; GUI::GetSetting(this, "absolute", absolute); float Z; GUI::GetSetting(this, "z", Z); if (absolute) return Z; else { if (GetParent()) return GetParent()->GetBufferedZ() + Z; else { // 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 Z; } } } void IGUIObject::RegisterScriptHandler(const CStr& Action, const CStr& Code, CGUI* pGUI) { if(!GetGUI()) throw PSERROR_GUI_OperationNeedsGUIObject(); JSContext* cx = pGUI->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::RootedValue globalVal(cx, pGUI->GetGlobalObject()); JS::RootedObject globalObj(cx, &globalVal.toObject()); const int paramCount = 1; const char* paramNames[paramCount] = { "mouse" }; // Location to report errors from CStr CodeName = GetName()+" "+Action; // Generate a unique name static int x = 0; char buf[64]; sprintf_s(buf, ARRAY_SIZE(buf), "__eventhandler%d (%s)", x++, Action.c_str()); JS::CompileOptions options(cx); options.setFileAndLine(CodeName.c_str(), 0); options.setCompileAndGo(true); JS::RootedFunction func(cx, JS_CompileFunction(cx, globalObj, buf, paramCount, paramNames, Code.c_str(), Code.length(), options)); if (!func) return; // JS will report an error message JS::RootedObject funcObj(cx, JS_GetFunctionObject(func)); SetScriptHandler(Action, funcObj); } void IGUIObject::SetScriptHandler(const CStr& Action, JS::HandleObject Function) { // m_ScriptHandlers is only rooted after SetGUI() has been called (which sets up the GC trace callbacks), // so we can't safely store objects in it if the GUI hasn't been set yet. ENSURE(m_pGUI && "A GUI must be associated with the GUIObject before adding ScriptHandlers!"); m_ScriptHandlers[Action] = JS::Heap(Function); } 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); } void IGUIObject::ScriptEvent(const CStr& Action) { auto it = m_ScriptHandlers.find(Action); if (it == m_ScriptHandlers.end()) return; JSContext* cx = m_pGUI->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); // Set up the 'mouse' parameter JS::RootedValue mouse(cx); m_pGUI->GetScriptInterface()->Eval("({})", &mouse); m_pGUI->GetScriptInterface()->SetProperty(mouse, "x", m_pGUI->m_MousePos.x, false); m_pGUI->GetScriptInterface()->SetProperty(mouse, "y", m_pGUI->m_MousePos.y, false); m_pGUI->GetScriptInterface()->SetProperty(mouse, "buttons", m_pGUI->m_MouseButtons, false); JS::AutoValueVector paramData(cx); paramData.append(mouse); JS::RootedObject obj(cx, GetJSObject()); JS::RootedValue handlerVal(cx, JS::ObjectValue(*it->second)); JS::RootedValue result(cx); bool ok = JS_CallFunctionValue(cx, obj, handlerVal, paramData, &result); if (!ok) { // We have no way to propagate the script exception, so just ignore it // and hope the caller checks JS_IsExceptionPending } } void IGUIObject::ScriptEvent(const CStr& Action, JS::HandleValue Argument) { auto it = m_ScriptHandlers.find(Action); if (it == m_ScriptHandlers.end()) return; JSContext* cx = m_pGUI->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::AutoValueVector paramData(cx); paramData.append(Argument.get()); JS::RootedObject obj(cx, GetJSObject()); JS::RootedValue handlerVal(cx, JS::ObjectValue(*it->second)); JS::RootedValue result(cx); bool ok = JS_CallFunctionValue(cx, obj, handlerVal, paramData, &result); if (!ok) { JS_ReportError(cx, "Errors executing script action \"%s\"", Action.c_str()); } } JSObject* IGUIObject::GetJSObject() { JSContext* cx = m_pGUI->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); // Cache the object when somebody first asks for it, because otherwise // we end up doing far too much object allocation. TODO: Would be nice to // not have these objects hang around forever using up memory, though. if (m_JSObject.uninitialized()) { m_JSObject.set(cx, m_pGUI->GetScriptInterface()->CreateCustomObject("GUIObject")); JS_SetPrivate(m_JSObject.get(), this); } return m_JSObject.get(); } 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 (m_Name.substr(0, 10) == "__internal") return CStr("[unnamed object]"); else return m_Name; } void IGUIObject::SetFocus() { GetGUI()->m_FocusedObject = this; } bool IGUIObject::IsFocused() const { return GetGUI()->m_FocusedObject == this; } bool IGUIObject::IsRootObject() const { return (GetGUI() != 0 && m_pParent == GetGUI()->m_BaseObject); } void IGUIObject::TraceMember(JSTracer* trc) { for (auto& handler : m_ScriptHandlers) JS_CallHeapObjectTracer(trc, &handler.second, "IGUIObject::m_ScriptHandlers"); } PSRETURN IGUIObject::LogInvalidSettings(const CStr8& Setting) const { LOGWARNING("IGUIObject: setting %s was not found on an object", Setting.c_str()); return PSRETURN_GUI_InvalidSetting; }