Index: ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItems.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItems.js (revision 22919) +++ ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItems.js (revision 22920) @@ -1,194 +1,202 @@ var g_MainMenuItems = [ { "caption": translate("Learn To Play"), "tooltip": translate("Learn how to play, start the tutorial, discover the technology trees, and the history behind the civilizations"), "submenu": [ { "caption": translate("Manual"), "tooltip": translate("Open the 0 A.D. Game Manual."), "onPress": () => { Engine.PushGuiPage("page_manual.xml"); } }, { "caption": translate("Tutorial"), "tooltip": translate("Start the economic tutorial."), "onPress": () => { Engine.SwitchGuiPage("page_gamesetup.xml", { "tutorial": true }); } }, { "caption": translate("Structure Tree"), "tooltip": colorizeHotkey(translate("%(hotkey)s: View the structure tree of civilizations featured in 0 A.D."), "structree"), "hotkey": "structree", "onPress": () => { - Engine.PushGuiPage("page_structree.xml"); - } + let callback = data => { + if (data.nextPage) + Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, callback); + }; + Engine.PushGuiPage("page_structree.xml", {}, callback); + }, }, { "caption": translate("History"), "tooltip": colorizeHotkey(translate("%(hotkey)s: Learn about the many civilizations featured in 0 A.D."), "civinfo"), "hotkey": "civinfo", "onPress": () => { - Engine.PushGuiPage("page_civinfo.xml"); + let callback = data => { + if (data.nextPage) + Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, callback); + }; + Engine.PushGuiPage("page_civinfo.xml", {}, callback); } } ] }, { "caption": translate("Single Player"), "tooltip": translate("Click here to start a new single player game."), "submenu": [ { "caption": translate("Matches"), "tooltip": translate("Start the economic tutorial."), "onPress": () => { Engine.SwitchGuiPage("page_gamesetup.xml", {}); } }, { "caption": translate("Campaigns"), "tooltip": translate("Relive history through historical military campaigns. \\[NOT YET IMPLEMENTED]"), "enabled": false }, { "caption": translate("Load Game"), "tooltip": translate("Click here to load a saved game."), "onPress": () => { Engine.PushGuiPage("page_loadgame.xml", { "type": "offline" }); } }, { "caption": translate("Replays"), "tooltip": translate("Playback previous games."), "onPress": () => { Engine.SwitchGuiPage("page_replaymenu.xml", { "replaySelectionData": { "filters": { "singleplayer": "Singleplayer" } } }); } } ] }, { "caption": translate("Multiplayer"), "tooltip": translate("Fight against one or more human players in a multiplayer game."), "submenu": [ { // Translation: Join a game by specifying the host's IP address. "caption": translate("Join Game"), "tooltip": translate("Joining an existing multiplayer game."), "onPress": () => { Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "join" }); } }, { "caption": translate("Host Game"), "tooltip": translate("Host a multiplayer game."), "onPress": () => { Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "host" }); } }, { "caption": translate("Game Lobby"), "tooltip": colorizeHotkey(translate("%(hotkey)s: Launch the multiplayer lobby to join and host publicly visible games and chat with other players."), "lobby") + (Engine.StartXmppClient ? "" : translate("Launch the multiplayer lobby. \\[DISABLED BY BUILD]")), "enabled": !!Engine.StartXmppClient, "hotkey": "lobby", "onPress": () => { if (Engine.StartXmppClient) Engine.PushGuiPage("page_prelobby_entrance.xml"); } }, { "caption": translate("Replays"), "tooltip": translate("Playback previous games."), "onPress": () => { Engine.SwitchGuiPage("page_replaymenu.xml", { "replaySelectionData": { "filters": { "singleplayer": "Multiplayer" } } }); } } ] }, { "caption": translate("Settings"), "tooltip": translate("Game options and scenario design tools."), "submenu": [ { "caption": translate("Options"), "tooltip": translate("Adjust game settings."), "onPress": () => { Engine.PushGuiPage("page_options.xml"); } }, { "caption": translate("Language"), "tooltip": translate("Choose the language of the game."), "onPress": () => { Engine.PushGuiPage("page_locale.xml"); } }, { "caption": translate("Mod Selection"), "tooltip": translate("Select and download mods for the game."), "onPress": () => { Engine.SwitchGuiPage("page_modmod.xml"); } }, { "caption": translate("Welcome Screen"), "tooltip": translate("Show the Welcome Screen. Useful if you hid it by mistake."), "onPress": () => { Engine.PushGuiPage("page_splashscreen.xml"); } } ] }, { "caption": translate("Scenario Editor"), "tooltip": translate('Open the Atlas Scenario Editor in a new window. You can run this more reliably by starting the game with the command-line argument "-editor".'), "onPress": () => { if (Engine.AtlasIsAvailable()) messageBox( 400, 200, translate("Are you sure you want to quit 0 A.D. and open the Scenario Editor?"), translate("Confirmation"), [translate("No"), translate("Yes")], [null, Engine.RestartInAtlas]); else messageBox( 400, 200, translate("The scenario editor is not available or failed to load. See the game logs for additional information."), translate("Error")); } }, { "caption": translate("Exit"), "tooltip": translate("Exits the game."), "onPress": () => { messageBox( 400, 200, translate("Are you sure you want to quit 0 A.D.?"), translate("Confirmation"), [translate("No"), translate("Yes")], [null, Engine.Exit]); } } ]; Index: ps/trunk/source/gui/CGUI.cpp =================================================================== --- ps/trunk/source/gui/CGUI.cpp (revision 22919) +++ ps/trunk/source/gui/CGUI.cpp (revision 22920) @@ -1,1386 +1,1386 @@ /* 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 #include #include "GUI.h" // Types - when including them into the engine. #include "CButton.h" #include "CChart.h" #include "CCheckBox.h" #include "CDropDown.h" #include "CImage.h" #include "CInput.h" #include "CList.h" #include "COList.h" #include "CProgressBar.h" #include "CRadioButton.h" #include "CSlider.h" #include "CText.h" #include "CTooltip.h" #include "MiniMap.h" #include "graphics/FontMetrics.h" #include "graphics/ShaderManager.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 "scripting/ScriptFunctions.h" #include "scriptinterface/ScriptInterface.h" 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.count(hotkey)) + if (m_GlobalHotkeys.count(hotkey) && ev->ev.type == SDL_HOTKEYDOWN) { HotkeyInputHandler(ev); 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 = NULL; // 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 NULL 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(NULL), m_InternalNameNumber(0) { m_ScriptInterface.reset(new ScriptInterface("Engine", "GUIPage", runtime)); m_ScriptInterface->SetCallbackData(this); GuiScriptingInit(*m_ScriptInterface); m_ScriptInterface->LoadGlobalScripts(); m_BaseObject = new CGUIDummyObject(*this); } CGUI::~CGUI() { Destroy(); if (m_BaseObject) delete m_BaseObject; } IGUIObject* CGUI::ConstructObject(const CStr& str) { if (m_ObjectTypes.count(str) > 0) return (*m_ObjectTypes[str])(*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()); } } 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::Destroy() { // We can use the map to delete all // now we don't want to cancel all if one Destroy fails for (const std::pair& p : m_pAllObjects) { try { p.second->Destroy(); } catch (PSERROR_GUI& e) { UNUSED2(e); debug_warn(L"CGUI::Destroy error"); // TODO Gee: Handle } delete p.second; } m_pAllObjects.clear(); for (const std::pair& p : m_Sprites) delete p.second; m_Sprites.clear(); m_Icons.clear(); } void CGUI::UpdateResolution() { // Update ALL cached m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize); } void CGUI::AddObject(IGUIObject* pObject) { 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&) { throw; } } void CGUI::UpdateObjects() { // We'll fill a temporary map until we know everything succeeded map_pObjects AllObjects; try { // Fill freshly m_BaseObject->RecurseObject(nullptr, &IGUIObject::AddToPointersMap, AllObjects); } catch (PSERROR_GUI&) { throw; } // Else actually update the real one m_pAllObjects.swap(AllObjects); } bool CGUI::ObjectExists(const CStr& Name) const { return m_pAllObjects.count(Name) != 0; } IGUIObject* CGUI::FindObjectByName(const CStr& Name) const { map_pObjects::const_iterator it = m_pAllObjects.find(Name); if (it == m_pAllObjects.end()) return NULL; else return it->second; } IGUIObject* CGUI::FindObjectUnderMouse() const { IGUIObject* pNearest = NULL; 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; } } //=================================================================== // 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