Index: ps/trunk/source/gui/CGUI.cpp =================================================================== --- ps/trunk/source/gui/CGUI.cpp (revision 23066) +++ ps/trunk/source/gui/CGUI.cpp (revision 23067) @@ -1,1346 +1,1302 @@ /* 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 "CGUI.h" #include "gui/IGUIScrollBar.h" #include "gui/ObjectTypes/CButton.h" #include "gui/ObjectTypes/CChart.h" #include "gui/ObjectTypes/CCheckBox.h" #include "gui/ObjectTypes/CDropDown.h" #include "gui/ObjectTypes/CImage.h" #include "gui/ObjectTypes/CInput.h" #include "gui/ObjectTypes/CList.h" #include "gui/ObjectTypes/CMiniMap.h" #include "gui/ObjectTypes/COList.h" #include "gui/ObjectTypes/CProgressBar.h" #include "gui/ObjectTypes/CRadioButton.h" #include "gui/ObjectTypes/CSlider.h" #include "gui/ObjectTypes/CText.h" #include "gui/ObjectTypes/CTooltip.h" #include "gui/Scripting/ScriptFunctions.h" #include "i18n/L10n.h" #include "lib/bits.h" #include "lib/input.h" #include "lib/sysdep/sysdep.h" #include "lib/timer.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include extern int g_yres; 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 InReaction CGUI::HandleEvent(const SDL_Event_* ev) { InReaction ret = IN_PASS; if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYUP) { const char* hotkey = static_cast(ev->ev.user.data1); if (m_GlobalHotkeys.find(hotkey) != m_GlobalHotkeys.end() && ev->ev.type == SDL_HOTKEYDOWN) { ret = IN_HANDLED; JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedObject globalObj(cx, &GetGlobalObject().toObject()); JS::RootedValue result(cx); JS_CallFunctionValue(cx, globalObj, m_GlobalHotkeys[hotkey], JS::HandleValueArray::empty(), &result); } std::map >::iterator it = m_HotkeyObjects.find(hotkey); if (it != m_HotkeyObjects.end()) for (IGUIObject* const& obj : it->second) { if (ev->ev.type == SDL_HOTKEYDOWN) ret = obj->SendEvent(GUIM_PRESSED, "press"); else ret = obj->SendEvent(GUIM_RELEASED, "release"); } } 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 = CPos((float)ev->ev.motion.x / g_GuiScale, (float)ev->ev.motion.y / g_GuiScale); SGUIMessage msg(GUIM_MOUSE_MOTION); m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &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(ev->ev.button.button); break; default: break; } } // Update m_MousePos (for delayed mouse button events) CPos oldMousePos = m_MousePos; if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP) { m_MousePos = CPos((float)ev->ev.button.x / g_GuiScale, (float)ev->ev.button.y / g_GuiScale); } // Only one object can be hovered IGUIObject* pNearest = nullptr; // TODO Gee: (2004-09-08) Big TODO, don't do the below if the SDL_Event is something like a keypress! - try { PROFILE("mouse events"); // TODO Gee: Optimizations needed! // these two recursive function are quite overhead heavy. // pNearest will after this point at the hovered object, possibly nullptr pNearest = FindObjectUnderMouse(); // 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::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast(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->SendEvent(GUIM_MOUSE_PRESS_LEFT, "mouseleftpress"); break; case SDL_BUTTON_RIGHT: if (pNearest) ret = pNearest->SendEvent(GUIM_MOUSE_PRESS_RIGHT, "mouserightpress"); break; default: break; } } else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest) { if (ev->ev.wheel.y < 0) ret = pNearest->SendEvent(GUIM_MOUSE_WHEEL_DOWN, "mousewheeldown"); else if (ev->ev.wheel.y > 0) ret = pNearest->SendEvent(GUIM_MOUSE_WHEEL_UP, "mousewheelup"); } 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->SendEvent(GUIM_MOUSE_DBLCLICK_LEFT, "mouseleftdoubleclick"); else ret = pNearest->SendEvent(GUIM_MOUSE_RELEASE_LEFT, "mouseleftrelease"); } 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->SendEvent(GUIM_MOUSE_DBLCLICK_RIGHT, "mouserightdoubleclick"); else ret = pNearest->SendEvent(GUIM_MOUSE_RELEASE_RIGHT, "mouserightrelease"); } 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::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast(pNearest)); } } - catch (PSERROR_GUI& e) - { - UNUSED2(e); - debug_warn(L"CGUI::HandleEvent error"); - // TODO Gee: Handle - } // 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(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; // Handle keys for input boxes if (GetFocusedObject()) { if ((ev->ev.type == SDL_KEYDOWN && ev->ev.key.keysym.sym != SDLK_ESCAPE && !g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] && !g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) || ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING) { ret = GetFocusedObject()->ManuallyHandleEvent(ev); } // else will return IN_PASS because we never used the button. } return ret; } void CGUI::TickObjects() { const CStr action = "tick"; m_BaseObject.RecurseObject(nullptr, &IGUIObject::ScriptEvent, action); m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this); } void CGUI::SendEventToAll(const CStr& EventName) { // janwas 2006-03-03: spoke with Ykkrosh about EventName case. // when registering, case is converted to lower - this avoids surprise // if someone were to get the case wrong and then not notice their // handler is never called. however, until now, the other end // (sending events here) wasn't converting to lower case, // leading to a similar problem. // now fixed; case is irrelevant since all are converted to lower. const CStr EventNameLower = EventName.LowerCase(); m_BaseObject.RecurseObject(nullptr, &IGUIObject::ScriptEvent, EventNameLower); } void CGUI::SendEventToAll(const CStr& EventName, const JS::HandleValueArray& paramData) { const CStr EventNameLower = EventName.LowerCase(); m_BaseObject.RecurseObject(nullptr, &IGUIObject::ScriptEvent, EventNameLower, paramData); } CGUI::CGUI(const shared_ptr& runtime) : m_MouseButtons(0), m_FocusedObject(nullptr), m_InternalNameNumber(0), m_BaseObject(*this) { m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIPage", runtime)); m_ScriptInterface->SetCallbackData(this); GuiScriptingInit(*m_ScriptInterface); m_ScriptInterface->LoadGlobalScripts(); } CGUI::~CGUI() { for (const std::pair& p : m_pAllObjects) delete p.second; for (const std::pair& p : m_Sprites) delete p.second; } IGUIObject* CGUI::ConstructObject(const CStr& str) { std::map::iterator it = m_ObjectTypes.find(str); if (it != m_ObjectTypes.end()) return (*it->second)(*this); // Error reporting will be handled with the nullptr return. return nullptr; } void CGUI::Initialize() { // Add base types! // You can also add types outside the GUI to extend the flexibility of the GUI. // Pyrogenesis though will have all the object types inserted from here. AddObjectType("empty", &CGUIDummyObject::ConstructObject); AddObjectType("button", &CButton::ConstructObject); AddObjectType("image", &CImage::ConstructObject); AddObjectType("text", &CText::ConstructObject); AddObjectType("checkbox", &CCheckBox::ConstructObject); AddObjectType("radiobutton", &CRadioButton::ConstructObject); AddObjectType("progressbar", &CProgressBar::ConstructObject); AddObjectType("minimap", &CMiniMap::ConstructObject); AddObjectType("input", &CInput::ConstructObject); AddObjectType("list", &CList::ConstructObject); AddObjectType("olist", &COList::ConstructObject); AddObjectType("dropdown", &CDropDown::ConstructObject); AddObjectType("tooltip", &CTooltip::ConstructObject); AddObjectType("chart", &CChart::ConstructObject); AddObjectType("slider", &CSlider::ConstructObject); } void CGUI::Draw() { // Clear the depth buffer, so the GUI is // drawn on top of everything else glClear(GL_DEPTH_BUFFER_BIT); - try - { - m_BaseObject.RecurseObject(&IGUIObject::IsHidden, &IGUIObject::Draw); - } - catch (PSERROR_GUI& e) - { - LOGERROR("GUI draw error: %s", e.what()); - } + m_BaseObject.RecurseObject(&IGUIObject::IsHidden, &IGUIObject::Draw); } void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, int CellID, const float& Z, const CRect& Rect, const CRect& UNUSED(Clipping)) { // If the sprite doesn't exist (name == ""), don't bother drawing anything if (!Sprite) return; // TODO: Clipping? Sprite.Draw(*this, Rect, CellID, m_Sprites, Z); } void CGUI::UpdateResolution() { - // Update ALL cached m_BaseObject.RecurseObject(nullptr, &IGUIObject::UpdateCachedSize); } -void CGUI::AddObject(IGUIObject* pObject) +bool CGUI::AddObject(IGUIObject& parent, IGUIObject& child) { - try - { - m_BaseObject.AddChild(pObject); - - // Cache tree - pObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize); - - SGUIMessage msg(GUIM_LOAD); - pObject->RecurseObject(nullptr, &IGUIObject::HandleMessage, msg); - } - catch (PSERROR_GUI&) + if (child.m_Name.empty()) { - throw; + LOGERROR("Can't register an object without name!"); + return false; } -} -void CGUI::UpdateObjects() -{ - // We'll fill a temporary map until we know everything succeeded - map_pObjects AllObjects; - - try + if (m_pAllObjects.find(child.m_Name) != m_pAllObjects.end()) { - // Fill freshly - m_BaseObject.RecurseObject(nullptr, &IGUIObject::AddToPointersMap, AllObjects); - } - catch (PSERROR_GUI&) - { - throw; + LOGERROR("Can't register more than one object of the name %s", child.m_Name.c_str()); + return false; } - // Else actually update the real one - m_pAllObjects.swap(AllObjects); + m_pAllObjects[child.m_Name] = &child; + parent.AddChild(child); + return true; } bool CGUI::ObjectExists(const CStr& Name) const { return m_pAllObjects.find(Name) != m_pAllObjects.end(); } IGUIObject* CGUI::FindObjectByName(const CStr& Name) const { map_pObjects::const_iterator it = m_pAllObjects.find(Name); if (it == m_pAllObjects.end()) return nullptr; return it->second; } IGUIObject* CGUI::FindObjectUnderMouse() { IGUIObject* pNearest = nullptr; m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::ChooseMouseOverAndClosest, pNearest); return pNearest; } 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::SetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag) { if (!hotkeyTag.empty()) m_HotkeyObjects[hotkeyTag].push_back(pObject); } void CGUI::UnsetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag) { if (hotkeyTag.empty()) return; std::vector& assignment = m_HotkeyObjects[hotkeyTag]; assignment.erase( std::remove_if( assignment.begin(), assignment.end(), [&pObject](const IGUIObject* hotkeyObject) { return pObject == hotkeyObject; }), assignment.end()); } void CGUI::SetGlobalHotkey(const CStr& hotkeyTag, JS::HandleValue function) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); if (hotkeyTag.empty()) { JS_ReportError(cx, "Cannot assign a function to an empty hotkey identifier!"); return; } if (!function.isObject() || !JS_ObjectIsFunction(cx, &function.toObject())) { JS_ReportError(cx, "Cannot assign non-function value to global hotkey '%s'", hotkeyTag.c_str()); return; } UnsetGlobalHotkey(hotkeyTag); m_GlobalHotkeys[hotkeyTag].init(cx, function); } void CGUI::UnsetGlobalHotkey(const CStr& hotkeyTag) { m_GlobalHotkeys.erase(hotkeyTag); } const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const { std::map::const_iterator it = m_ScrollBarStyles.find(style); if (it == m_ScrollBarStyles.end()) return nullptr; return &it->second; } /** * @callgraph */ void CGUI::LoadXmlFile(const VfsPath& Filename, boost::unordered_set& Paths) { Paths.insert(Filename); CXeromyces XeroFile; if (XeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK) return; XMBElement node = XeroFile.GetRoot(); - CStr root_name(XeroFile.GetElementString(node.GetNodeName())); - try - { - if (root_name == "objects") - { - Xeromyces_ReadRootObjects(node, &XeroFile, Paths); - // Re-cache all values so these gets cached too. - //UpdateResolution(); - } - else if (root_name == "sprites") - Xeromyces_ReadRootSprites(node, &XeroFile); - else if (root_name == "styles") - Xeromyces_ReadRootStyles(node, &XeroFile); - else if (root_name == "setup") - Xeromyces_ReadRootSetup(node, &XeroFile); - else - debug_warn(L"CGUI::LoadXmlFile error"); - } - catch (PSERROR_GUI& e) - { - LOGERROR("Errors loading GUI file %s (%u)", Filename.string8(), e.getCode()); - return; - } + if (root_name == "objects") + Xeromyces_ReadRootObjects(node, &XeroFile, Paths); + else if (root_name == "sprites") + Xeromyces_ReadRootSprites(node, &XeroFile); + else if (root_name == "styles") + Xeromyces_ReadRootStyles(node, &XeroFile); + else if (root_name == "setup") + Xeromyces_ReadRootSetup(node, &XeroFile); + else + LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name.c_str()); +} + +void CGUI::LoadedXmlFiles() +{ + m_BaseObject.RecurseObject(nullptr, &IGUIObject::UpdateCachedSize); + + SGUIMessage msg(GUIM_LOAD); + m_BaseObject.RecurseObject(nullptr, &IGUIObject::HandleMessage, msg); + + SendEventToAll("load"); } //=================================================================== // XML Reading Xeromyces Specific Sub-Routines //=================================================================== void CGUI::Xeromyces_ReadRootObjects(XMBElement Element, CXeromyces* pFile, boost::unordered_set& Paths) { int el_script = pFile->GetElementID("script"); std::vector > subst; // Iterate main children // they should all be or