Index: ps/trunk/source/gui/CDropDown.cpp =================================================================== --- ps/trunk/source/gui/CDropDown.cpp (revision 22974) +++ ps/trunk/source/gui/CDropDown.cpp (revision 22975) @@ -1,508 +1,505 @@ /* Copyright (C) 2019 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 "CDropDown.h" #include "gui/CGUI.h" #include "gui/CGUIColor.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" #include "ps/Profile.h" CDropDown::CDropDown(CGUI& pGUI) : CList(pGUI), IGUIObject(pGUI), m_Open(false), m_HideScrollBar(false), m_ElementHighlight(-1) { AddSetting("button_width"); AddSetting("dropdown_size"); AddSetting("dropdown_buffer"); AddSetting("minimum_visible_items"); -// AddSetting("sound_closed"); - AddSetting("sound_disabled"); AddSetting("sound_enter"); AddSetting("sound_leave"); AddSetting("sound_opened"); - AddSetting("sprite"); // Background that sits around the size + // Setting "sprite" is registered by CList and used as the background AddSetting("sprite_disabled"); AddSetting("sprite_list"); // Background of the drop down list AddSetting("sprite2"); // Button that sits to the right AddSetting("sprite2_over"); AddSetting("sprite2_pressed"); AddSetting("sprite2_disabled"); AddSetting("text_valign"); // Add these in CList! And implement TODO //AddSetting("textcolor_over"); //AddSetting("textcolor_pressed"); - AddSetting("textcolor_selected"); AddSetting("textcolor_disabled"); // Scrollbar is forced to be true. SetSetting("scrollbar", true, true); } CDropDown::~CDropDown() { } void CDropDown::SetupText() { SetupListRect(); CList::SetupText(); } void CDropDown::UpdateCachedSize() { CList::UpdateCachedSize(); SetupText(); } void CDropDown::HandleMessage(SGUIMessage& Message) { // Important switch (Message.type) { case GUIM_SETTINGS_UPDATED: { // Update cached list rect if (Message.value == "size" || Message.value == "absolute" || Message.value == "dropdown_size" || Message.value == "dropdown_buffer" || Message.value == "minimum_visible_items" || Message.value == "scrollbar_style" || Message.value == "button_width") { SetupListRect(); } break; } case GUIM_MOUSE_MOTION: { if (!m_Open) break; CPos mouse = m_pGUI.GetMousePos(); if (!GetListRect().PointInside(mouse)) break; const CGUIList& pList = GetSetting("list"); const bool scrollbar = GetSetting("scrollbar"); const float scroll = scrollbar ? GetScrollBar(0).GetPos() : 0.f; CRect rect = GetListRect(); mouse.y += scroll; int set = -1; for (int i = 0; i < static_cast(pList.m_Items.size()); ++i) { if (mouse.y >= rect.top + m_ItemsYPositions[i] && mouse.y < rect.top + m_ItemsYPositions[i+1] && // mouse is not over scroll-bar (m_HideScrollBar || mouse.x < GetScrollBar(0).GetOuterRect().left || mouse.x > GetScrollBar(0).GetOuterRect().right)) { set = i; } } if (set != -1) { m_ElementHighlight = set; //UpdateAutoScroll(); } break; } case GUIM_MOUSE_ENTER: { if (GetSetting("enabled")) PlaySound("sound_enter"); break; } case GUIM_MOUSE_LEAVE: { m_ElementHighlight = GetSetting("selected"); if (GetSetting("enabled")) PlaySound("sound_leave"); break; } // We can't inherent this routine from CList, because we need to include // a mouse click to open the dropdown, also the coordinates are changed. case GUIM_MOUSE_PRESS_LEFT: { if (!GetSetting("enabled")) { PlaySound("sound_disabled"); break; } if (!m_Open) { const CGUIList& pList = GetSetting("list"); if (pList.m_Items.empty()) return; m_Open = true; GetScrollBar(0).SetZ(GetBufferedZ()); m_ElementHighlight = GetSetting("selected"); // Start at the position of the selected item, if possible. GetScrollBar(0).SetPos(m_ItemsYPositions.empty() ? 0 : m_ItemsYPositions[m_ElementHighlight] - 60); PlaySound("sound_opened"); return; // overshadow } else { const CPos& mouse = m_pGUI.GetMousePos(); // If the regular area is pressed, then abort, and close. if (m_CachedActualSize.PointInside(mouse)) { m_Open = false; GetScrollBar(0).SetZ(GetBufferedZ()); PlaySound("sound_closed"); return; // overshadow } if (m_HideScrollBar || mouse.x < GetScrollBar(0).GetOuterRect().left || mouse.x > GetScrollBar(0).GetOuterRect().right || mouse.y < GetListRect().top) { m_Open = false; GetScrollBar(0).SetZ(GetBufferedZ()); } } break; } case GUIM_MOUSE_WHEEL_DOWN: { // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar. if (m_Open || !GetSetting("enabled")) break; m_ElementHighlight = GetSetting("selected"); if (m_ElementHighlight + 1 >= (int)m_ItemsYPositions.size() - 1) break; ++m_ElementHighlight; SetSetting("selected", m_ElementHighlight, true); break; } case GUIM_MOUSE_WHEEL_UP: { // Don't switch elements by scrolling when open, causes a confusing interaction between this and the scrollbar. if (m_Open || !GetSetting("enabled")) break; m_ElementHighlight = GetSetting("selected"); if (m_ElementHighlight - 1 < 0) break; --m_ElementHighlight; SetSetting("selected", m_ElementHighlight, true); break; } case GUIM_LOST_FOCUS: { if (m_Open) PlaySound("sound_closed"); m_Open = false; break; } case GUIM_LOAD: SetupListRect(); break; default: break; } // Important that this is after, so that overshadowed implementations aren't processed CList::HandleMessage(Message); // As HandleMessage functions return void, we need to manually verify // whether the child list's items were modified. if (CList::GetModified()) SetupText(); } InReaction CDropDown::ManuallyHandleEvent(const SDL_Event_* ev) { InReaction result = IN_PASS; bool update_highlight = false; if (ev->ev.type == SDL_KEYDOWN) { int szChar = ev->ev.key.keysym.sym; switch (szChar) { case '\r': m_Open = false; result = IN_HANDLED; break; case SDLK_HOME: case SDLK_END: case SDLK_UP: case SDLK_DOWN: case SDLK_PAGEUP: case SDLK_PAGEDOWN: if (!m_Open) return IN_PASS; // Set current selected item to highlighted, before // then really processing these in CList::ManuallyHandleEvent() SetSetting("selected", m_ElementHighlight, true); update_highlight = true; break; default: // If we have inputed a character try to get the closest element to it. // TODO: not too nice and doesn't deal with dashes. if (m_Open && ((szChar >= SDLK_a && szChar <= SDLK_z) || szChar == SDLK_SPACE || (szChar >= SDLK_0 && szChar <= SDLK_9) || (szChar >= SDLK_KP_0 && szChar <= SDLK_KP_9))) { // arbitrary 1 second limit to add to string or start fresh. // maximal amount of characters is 100, which imo is far more than enough. if (timer_Time() - m_TimeOfLastInput > 1.0 || m_InputBuffer.length() >= 100) m_InputBuffer = szChar; else m_InputBuffer += szChar; m_TimeOfLastInput = timer_Time(); const CGUIList& pList = GetSetting("list"); // let's look for the closest element // basically it's alphabetic order and "as many letters as we can get". int closest = -1; int bestIndex = -1; int difference = 1250; for (int i = 0; i < static_cast(pList.m_Items.size()); ++i) { int indexOfDifference = 0; int diff = 0; for (size_t j = 0; j < m_InputBuffer.length(); ++j) { diff = std::abs(static_cast(pList.m_Items[i].GetRawString().LowerCase()[j]) - static_cast(m_InputBuffer[j])); if (diff == 0) indexOfDifference = j+1; else break; } if (indexOfDifference > bestIndex || (indexOfDifference >= bestIndex && diff < difference)) { bestIndex = indexOfDifference; closest = i; difference = diff; } } // let's select the closest element. There should basically always be one. if (closest != -1) { SetSetting("selected", closest, true); update_highlight = true; GetScrollBar(0).SetPos(m_ItemsYPositions[closest] - 60); } result = IN_HANDLED; } break; } } if (CList::ManuallyHandleEvent(ev) == IN_HANDLED) result = IN_HANDLED; if (update_highlight) m_ElementHighlight = GetSetting("selected"); return result; } void CDropDown::SetupListRect() { extern int g_yres; extern float g_GuiScale; const float yres = g_yres / g_GuiScale; const float size = GetSetting("dropdown_size"); const float buffer = GetSetting("dropdown_buffer"); const u32 minimumVisibleItems = GetSetting("minimum_visible_items"); if (m_ItemsYPositions.empty()) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, m_CachedActualSize.bottom + buffer + size); m_HideScrollBar = false; } // Too many items so use a scrollbar else if (m_ItemsYPositions.back() > size) { // Place items below if at least some items can be placed below if (m_CachedActualSize.bottom + buffer + size <= yres) m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, m_CachedActualSize.bottom + buffer + size); else if ((m_ItemsYPositions.size() > minimumVisibleItems && yres - m_CachedActualSize.bottom - buffer >= m_ItemsYPositions[minimumVisibleItems]) || m_CachedActualSize.top < yres - m_CachedActualSize.bottom) m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, yres); // Not enough space below, thus place items above else m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - buffer - size), m_CachedActualSize.right, m_CachedActualSize.top - buffer); m_HideScrollBar = false; } else { // Enough space below, no scrollbar needed if (m_CachedActualSize.bottom + buffer + m_ItemsYPositions.back() <= yres) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, m_CachedActualSize.bottom + buffer + m_ItemsYPositions.back()); m_HideScrollBar = true; } // Enough space below for some items, but not all, so place items below and use a scrollbar else if ((m_ItemsYPositions.size() > minimumVisibleItems && yres - m_CachedActualSize.bottom - buffer >= m_ItemsYPositions[minimumVisibleItems]) || m_CachedActualSize.top < yres - m_CachedActualSize.bottom) { m_CachedListRect = CRect(m_CachedActualSize.left, m_CachedActualSize.bottom + buffer, m_CachedActualSize.right, yres); m_HideScrollBar = false; } // Not enough space below, thus place items above. Hide the scrollbar accordingly else { m_CachedListRect = CRect(m_CachedActualSize.left, std::max(0.f, m_CachedActualSize.top - buffer - m_ItemsYPositions.back()), m_CachedActualSize.right, m_CachedActualSize.top - buffer); m_HideScrollBar = m_CachedActualSize.top > m_ItemsYPositions.back() + buffer; } } } CRect CDropDown::GetListRect() const { return m_CachedListRect; } bool CDropDown::IsMouseOver() const { if (m_Open) { CRect rect(m_CachedActualSize.left, std::min(m_CachedActualSize.top, GetListRect().top), m_CachedActualSize.right, std::max(m_CachedActualSize.bottom, GetListRect().bottom)); return rect.PointInside(m_pGUI.GetMousePos()); } else return m_CachedActualSize.PointInside(m_pGUI.GetMousePos()); } void CDropDown::Draw() { const float bz = GetBufferedZ(); const float button_width = GetSetting("button_width"); const bool enabled = GetSetting("enabled"); const int cell_id = GetSetting("cell_id"); const int selected = GetSetting("selected"); CGUISpriteInstance& sprite = GetSetting(enabled ? "sprite" : "sprite_disabled"); CGUISpriteInstance& sprite2 = GetSetting("sprite2"); m_pGUI.DrawSprite(sprite, cell_id, bz, m_CachedActualSize); if (button_width > 0.f) { CRect rect(m_CachedActualSize.right-button_width, m_CachedActualSize.top, m_CachedActualSize.right, m_CachedActualSize.bottom); if (!enabled) { CGUISpriteInstance& sprite2_second = GetSetting("sprite2_disabled"); m_pGUI.DrawSprite(sprite2_second || sprite2, cell_id, bz + 0.05f, rect); } else if (m_Open) { CGUISpriteInstance& sprite2_second = GetSetting("sprite2_pressed"); m_pGUI.DrawSprite(sprite2_second || sprite2, cell_id, bz + 0.05f, rect); } else if (m_MouseHovering) { CGUISpriteInstance& sprite2_second = GetSetting("sprite2_over"); m_pGUI.DrawSprite(sprite2_second || sprite2, cell_id, bz + 0.05f, rect); } else m_pGUI.DrawSprite(sprite2, cell_id, bz + 0.05f, rect); } if (selected != -1) // TODO: Maybe check validity completely? { CRect cliparea(m_CachedActualSize.left, m_CachedActualSize.top, m_CachedActualSize.right-button_width, m_CachedActualSize.bottom); const CGUIColor& color = GetSetting(enabled ? "textcolor_selected" : "textcolor_disabled"); CPos pos(m_CachedActualSize.left, m_CachedActualSize.top); DrawText(selected, color, pos, bz+0.1f, cliparea); } // Disable scrollbar during drawing without sending a setting-changed message bool& scrollbar = GetSetting("scrollbar"); bool old = scrollbar; if (m_Open) { if (m_HideScrollBar) scrollbar = false; DrawList(m_ElementHighlight, "sprite_list", "sprite_selectarea", "textcolor"); if (m_HideScrollBar) scrollbar = old; } } // When a dropdown list is opened, it needs to be visible above all the other // controls on the page. The only way I can think of to do this is to increase // its z value when opened, so that it's probably on top. float CDropDown::GetBufferedZ() const { float bz = CList::GetBufferedZ(); if (m_Open) return std::min(bz + 500.f, 1000.f); // TODO - don't use magic number for max z value else return bz; } Index: ps/trunk/source/gui/IGUIObject.cpp =================================================================== --- ps/trunk/source/gui/IGUIObject.cpp (revision 22974) +++ ps/trunk/source/gui/IGUIObject.cpp (revision 22975) @@ -1,549 +1,548 @@ /* Copyright (C) 2019 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/CGUI.h" #include "gui/CGUISetting.h" #include "gui/scripting/JSInterface_GUITypes.h" #include "gui/scripting/JSInterface_IGUIObject.h" #include "ps/GameSetup/Config.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" #include "soundmanager/ISoundManager.h" IGUIObject::IGUIObject(CGUI& pGUI) : m_pGUI(pGUI), m_pParent(NULL), m_MouseHovering(false), m_LastClickTime() { AddSetting("enabled"); AddSetting("hidden"); AddSetting("size"); AddSetting("style"); AddSetting("hotkey"); AddSetting("z"); AddSetting("absolute"); AddSetting("ghost"); AddSetting("aspectratio"); AddSetting("tooltip"); AddSetting("tooltip_style"); // Setup important defaults SetSetting("hidden", false, true); SetSetting("ghost", false, true); SetSetting("enabled", true, true); SetSetting("absolute", true, true); } IGUIObject::~IGUIObject() { for (const std::pair& p : m_Settings) delete p.second; if (!m_ScriptHandlers.empty()) JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this); } //------------------------------------------------------------------- // Functions //------------------------------------------------------------------- void IGUIObject::AddChild(IGUIObject* pChild) { // ENSURE(pChild); pChild->SetParent(this); m_Children.push_back(pChild); { 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; } } template void IGUIObject::AddSetting(const CStr& Name) { - // This can happen due to inheritance if (SettingExists(Name)) - return; - - m_Settings[Name] = new CGUISetting(*this, Name); + LOGERROR("The setting '%s' already exists on the object '%s'!", Name.c_str(), GetPresentableName().c_str()); + else + m_Settings.emplace(Name, new CGUISetting(*this, Name)); } bool IGUIObject::SettingExists(const CStr& Setting) const { return m_Settings.count(Setting) == 1; } template T& IGUIObject::GetSetting(const CStr& Setting) { return static_cast* >(m_Settings.at(Setting))->m_pSetting; } template const T& IGUIObject::GetSetting(const CStr& Setting) const { return static_cast* >(m_Settings.at(Setting))->m_pSetting; } bool IGUIObject::SetSettingFromString(const CStr& Setting, const CStrW& Value, const bool SendMessage) { const std::map::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); } template void IGUIObject::SetSetting(const CStr& Setting, T& Value, const bool SendMessage) { PreSettingChange(Setting); static_cast* >(m_Settings[Setting])->m_pSetting = std::move(Value); SettingChanged(Setting, SendMessage); } template void IGUIObject::SetSetting(const CStr& Setting, const T& Value, const bool SendMessage) { PreSettingChange(Setting); static_cast* >(m_Settings[Setting])->m_pSetting = Value; SettingChanged(Setting, SendMessage); } void IGUIObject::PreSettingChange(const CStr& Setting) { if (Setting == "hotkey") m_pGUI.UnsetObjectHotkey(this, GetSetting(Setting)); } 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 (GetSetting(Setting)) RecurseObject(nullptr, &IGUIObject::ResetStates); } else if (Setting == "hotkey") m_pGUI.SetObjectHotkey(this, GetSetting(Setting)); if (SendMessage) { SGUIMessage msg(GUIM_SETTINGS_UPDATED, Setting); HandleMessage(msg); } } bool IGUIObject::IsMouseOver() const { return m_CachedActualSize.PointInside(m_pGUI.GetMousePos()); } bool IGUIObject::MouseOverIcon() { return false; } void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver) { if (pMouseOver == this) { if (!m_MouseHovering) SendEvent(GUIM_MOUSE_ENTER, "mouseenter"); m_MouseHovering = true; SendEvent(GUIM_MOUSE_OVER, "mousemove"); } else { if (m_MouseHovering) { m_MouseHovering = false; SendEvent(GUIM_MOUSE_LEAVE, "mouseleave"); } } } void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject) { if (!IsMouseOver()) 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::ResetStates() { // Notify the gui that we aren't hovered anymore UpdateMouseOver(nullptr); } void IGUIObject::UpdateCachedSize() { const CClientArea& ca = GetSetting("size"); const float aspectratio = GetSetting("aspectratio"); // 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 (!GetSetting("absolute") && 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(const CStr& StyleName) { if (!m_pGUI.HasStyle(StyleName)) debug_warn(L"IGUIObject::LoadStyle failed"); // 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& p : m_pGUI.GetStyle(StyleName).m_SettingsDefaults) { if (SettingExists(p.first)) SetSettingFromString(p.first, 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()); } } float IGUIObject::GetBufferedZ() const { const float Z = GetSetting("z"); if (GetSetting("absolute")) return Z; { 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) { 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.setIsRunOnce(false); JS::RootedFunction func(cx); JS::AutoObjectVector emptyScopeChain(cx); if (!JS::CompileFunction(cx, emptyScopeChain, options, buf, paramCount, paramNames, Code.c_str(), Code.length(), &func)) { LOGERROR("RegisterScriptHandler: Failed to compile the script for %s", Action.c_str()); return; } JS::RootedObject funcObj(cx, JS_GetFunctionObject(func)); SetScriptHandler(Action, funcObj); } void IGUIObject::SetScriptHandler(const CStr& Action, JS::HandleObject Function) { if (m_ScriptHandlers.empty()) JS_AddExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this); 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) { std::map >::iterator 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); const CPos& mousePos = m_pGUI.GetMousePos(); ScriptInterface::CreateObject( cx, &mouse, "x", mousePos.x, "y", mousePos.y, "buttons", m_pGUI.GetMouseButtons()); 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, const JS::HandleValueArray& paramData) { std::map >::iterator it = m_ScriptHandlers.find(Action); if (it == m_ScriptHandlers.end()) return; JSContext* cx = m_pGUI.GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); JS::RootedObject obj(cx, GetJSObject()); JS::RootedValue handlerVal(cx, JS::ObjectValue(*it->second)); JS::RootedValue result(cx); if (!JS_CallFunctionValue(cx, obj, handlerVal, paramData, &result)) JS_ReportError(cx, "Errors executing script action \"%s\"", Action.c_str()); } void IGUIObject::CreateJSObject() { JSContext* cx = m_pGUI.GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); m_JSObject.init(cx, m_pGUI.GetScriptInterface()->CreateCustomObject("GUIObject")); JS_SetPrivate(m_JSObject.get(), this); } 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.initialized()) CreateJSObject(); return m_JSObject.get(); } bool IGUIObject::IsHidden() const { // Statically initialise some strings, so we don't have to do // lots of allocation every time this function is called static const CStr strHidden("hidden"); return GetSetting(strHidden); } bool IGUIObject::IsHiddenOrGhost() const { static const CStr strGhost("ghost"); return IsHidden() || GetSetting(strGhost); } void IGUIObject::PlaySound(const CStr& settingName) const { if (!g_SoundManager) return; const CStrW& soundPath = GetSetting(settingName); if (!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 (m_Name.substr(0, 10) == "__internal") return CStr("[unnamed object]"); else return m_Name; } void IGUIObject::SetFocus() { m_pGUI.SetFocusedObject(this); } 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>& handler : m_ScriptHandlers) JS_CallObjectTracer(trc, &handler.second, "IGUIObject::m_ScriptHandlers"); } // Instantiate templated functions: // These functions avoid copies by working with a reference and move semantics. #define TYPE(T) \ template void IGUIObject::AddSetting(const CStr& Name); \ template T& IGUIObject::GetSetting(const CStr& Setting); \ template const T& IGUIObject::GetSetting(const CStr& Setting) const; \ template void IGUIObject::SetSetting(const CStr& Setting, T& Value, const bool SendMessage); \ #include "gui/GUItypes.h" #undef TYPE // Copying functions - discouraged except for primitives. #define TYPE(T) \ template void IGUIObject::SetSetting(const CStr& Setting, const T& Value, const bool SendMessage); \ #define GUITYPE_IGNORE_NONCOPYABLE #include "gui/GUItypes.h" #undef GUITYPE_IGNORE_NONCOPYABLE #undef TYPE