Index: ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js (revision 23102) +++ ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js (revision 23103) @@ -1,142 +1,150 @@ /** * This class sets up the main menu buttons, animates submenu that opens when * clicking on category buttons, assigns the defined actions and hotkeys to every button. */ class MainMenuItemHandler { constructor(menuItems) { this.menuItems = menuItems; this.lastTickTime = Date.now(); this.mainMenu = Engine.GetGUIObjectByName("mainMenu"); this.mainMenuButtons = Engine.GetGUIObjectByName("mainMenuButtons"); this.submenu = Engine.GetGUIObjectByName("submenu"); this.submenuButtons = Engine.GetGUIObjectByName("submenuButtons"); this.MainMenuPanelRightBorderTop = Engine.GetGUIObjectByName("MainMenuPanelRightBorderTop"); this.MainMenuPanelRightBorderBottom = Engine.GetGUIObjectByName("MainMenuPanelRightBorderBottom"); this.setupMenuButtons(this.mainMenuButtons.children, this.menuItems); this.setupHotkeys(this.menuItems); - this.mainMenu.onTick = this.onTick.bind(this); Engine.GetGUIObjectByName("closeMenuButton").onPress = this.closeSubmenu.bind(this); } setupMenuButtons(buttons, menuItems) { buttons.forEach((button, i) => { let item = menuItems[i]; button.hidden = !item; if (button.hidden) return; button.size = new GUISize( 0, (this.ButtonHeight + this.Margin) * i, 0, (this.ButtonHeight + this.Margin) * i + this.ButtonHeight, 0, 0, 100, 0); button.caption = item.caption; button.tooltip = item.tooltip; button.enabled = item.enabled === undefined || item.enabled; button.onPress = () => { this.closeSubmenu(); if (item.onPress) item.onPress(); else this.openSubmenu(i); }; button.hidden = false; }); if (buttons.length < menuItems.length) error("GUI page has space for " + buttons.length + " menu buttons, but " + menuItems.length + " items are provided!"); } setupHotkeys(menuItems) { for (let i in menuItems) { let item = menuItems[i]; if (item.onPress && item.hotkey) Engine.SetGlobalHotkey(item.hotkey, () => { this.closeSubmenu(); item.onPress(); }); if (item.submenu) this.setupHotkeys(item.submenu); } } openSubmenu(i) { this.setupMenuButtons(this.submenuButtons.children, this.menuItems[i].submenu); let top = this.mainMenuButtons.size.top + this.mainMenuButtons.children[i].size.top; this.submenu.size = new GUISize( this.submenu.size.left, top - this.Margin, this.submenu.size.right, top + (this.ButtonHeight + this.Margin) * this.menuItems[i].submenu.length); this.submenu.hidden = false; { let size = this.MainMenuPanelRightBorderTop.size; size.bottom = this.submenu.size.top + this.Margin; size.rbottom = 0; this.MainMenuPanelRightBorderTop.size = size; } { let size = this.MainMenuPanelRightBorderBottom.size; size.top = this.submenu.size.bottom; this.MainMenuPanelRightBorderBottom.size = size; } + + // Start animation + this.lastTickTime = Date.now(); + this.mainMenu.onTick = this.onTick.bind(this); } closeSubmenu() { this.submenu.hidden = true; this.submenu.size = this.mainMenu.size; let size = this.MainMenuPanelRightBorderTop.size; size.top = 0; size.bottom = 0; size.rbottom = 100; this.MainMenuPanelRightBorderTop.size = size; } onTick() { let now = Date.now(); + if (now == this.lastTickTime) + return; let maxOffset = this.mainMenu.size.right - this.submenu.size.left; let offset = Math.min(this.MenuSpeed * (now - this.lastTickTime), maxOffset); this.lastTickTime = now; - if (this.submenu.hidden || offset <= 0) + if (this.submenu.hidden || !offset) + { + delete this.mainMenu.onTick; return; + } let size = this.submenu.size; size.left += offset; size.right += offset; this.submenu.size = size; } } /** * Vertical size per button. */ MainMenuItemHandler.prototype.ButtonHeight = 28; /** * Distance between consecutive buttons. */ MainMenuItemHandler.prototype.Margin = 4; /** * Collapse / expansion speed in pixels per milliseconds used when animating the button menu size. */ MainMenuItemHandler.prototype.MenuSpeed = 1.2; Index: ps/trunk/binaries/data/mods/public/gui/pregame/SplashscreenHandler.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/pregame/SplashscreenHandler.js (revision 23102) +++ ps/trunk/binaries/data/mods/public/gui/pregame/SplashscreenHandler.js (revision 23103) @@ -1,68 +1,67 @@ class SplashScreenHandler { constructor(initData, hotloadData) { this.showSplashScreen = hotloadData ? hotloadData.showSplashScreen : initData && initData.isStartup; this.mainMenuPage = Engine.GetGUIObjectByName("mainMenuPage"); this.mainMenuPage.onTick = this.onFirstTick.bind(this); } getHotloadData() { // Only show splash screen(s) once at startup, but not again after hotloading return { "showSplashScreen": this.showSplashScreen }; } // Don't call this from the init function in order to not crash when opening the new page on init on hotloading // and not possibly crash when opening the new page on init and throwing a JS error. onFirstTick() { if (this.showSplashScreen) this.openPage(); - // TODO: support actually deleting the handler - this.mainMenuPage.onTick = () => {}; + delete this.mainMenuPage.onTick; } openPage() { this.showSplashScreen = false; if (Engine.ConfigDB_GetValue("user", "gui.splashscreen.enable") === "true" || Engine.ConfigDB_GetValue("user", "gui.splashscreen.version") < Engine.GetFileMTime("gui/splashscreen/splashscreen.txt")) Engine.PushGuiPage("page_splashscreen.xml", {}, this.showRenderPathMessage); else this.showRenderPathMessage(); } showRenderPathMessage() { // Warn about removing fixed render path if (Engine.Renderer_GetRenderPath() != "fixed") return; messageBox( 600, 300, "[font=\"sans-bold-16\"]" + sprintf(translate("%(warning)s You appear to be using non-shader (fixed function) graphics. This option will be removed in a future 0 A.D. release, to allow for more advanced graphics features. We advise upgrading your graphics card to a more recent, shader-compatible model."), { "warning": coloredText("Warning:", "200 20 20") }) + "\n\n" + // Translation: This is the second paragraph of a warning. The // warning explains that the user is using “non-shader“ graphics, // and that in the future this will not be supported by the game, so // the user will need a better graphics card. translate("Please press \"Read More\" for more information or \"OK\" to continue."), translate("WARNING!"), [translate("OK"), translate("Read More")], [ null, () => { Engine.OpenURL("https://www.wildfiregames.com/forum/index.php?showtopic=16734"); } ]); } } Index: ps/trunk/binaries/data/mods/public/gui/session/Menu.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/Menu.js (revision 23102) +++ ps/trunk/binaries/data/mods/public/gui/session/Menu.js (revision 23103) @@ -1,120 +1,119 @@ /** * This class constructs and positions the menu buttons and assigns the handlers defined in MenuButtons. */ class Menu { constructor(pauseControl, playerViewControl, chat) { this.menuButton = Engine.GetGUIObjectByName("menuButton"); this.menuButton.onPress = this.toggle.bind(this); registerHotkeyChangeHandler(this.rebuild.bind(this)); this.isOpen = false; this.lastTick = undefined; this.menuButtonPanel = Engine.GetGUIObjectByName("menuButtonPanel"); let menuButtons = this.menuButtonPanel.children; this.margin = menuButtons[0].size.top; this.buttonHeight = menuButtons[0].size.bottom; let handlerNames = this.getHandlerNames(); if (handlerNames.length > menuButtons.length) throw new Error( "There are " + handlerNames.length + " menu buttons defined, " + "but only " + menuButtons.length + " objects!"); this.buttons = handlerNames.map((handlerName, i) => { let handler = new MenuButtons.prototype[handlerName](menuButtons[i], pauseControl, playerViewControl, chat); this.initButton(handler, menuButtons[i], i); return handler; }); this.endPosition = this.margin + this.buttonHeight * (1 + handlerNames.length); let size = this.menuButtonPanel.size; size.top = -this.endPosition; size.bottom = 0; this.menuButtonPanel.size = size; } rebuild() { this.menuButton.tooltip = sprintf(translate("Press %(hotkey)s to toggle this menu."), { "hotkey": colorizeHotkey("%(hotkey)s", this.menuButton.hotkey), }); } /** * This function may be overwritten to change the button order. */ getHandlerNames() { return Object.keys(MenuButtons.prototype); } toggle() { this.isOpen = !this.isOpen; this.startAnimation(); } close() { this.isOpen = false; this.startAnimation(); } initButton(handler, button, i) { button.onPress = () => { this.close(); handler.onPress(); }; let size = button.size; size.top = this.buttonHeight * (i + 1) + this.margin; size.bottom = this.buttonHeight * (i + 2); button.size = size; button.hidden = false; } startAnimation() { this.lastTick = Date.now(); this.menuButtonPanel.onTick = this.onTick.bind(this); } /** * Animate menu panel. */ onTick() { let tickLength = Date.now() - this.lastTick; this.lastTick = Date.now(); let maxOffset = this.endPosition + ( this.isOpen ? -this.menuButtonPanel.size.bottom : +this.menuButtonPanel.size.top); if (maxOffset <= 0) { - // TODO: support actually deleting the handler - this.menuButtonPanel.onTick = () => {}; + delete this.menuButtonPanel.onTick; return; } let offset = Math.min(this.Speed * tickLength, maxOffset) * (this.isOpen ? +1 : -1); let size = this.menuButtonPanel.size; size.top += offset; size.bottom += offset; this.menuButtonPanel.size = size; } } /** * Number of pixels per millisecond to move. */ Menu.prototype.Speed = 1.2; Index: ps/trunk/source/gui/ObjectBases/IGUIObject.cpp =================================================================== --- ps/trunk/source/gui/ObjectBases/IGUIObject.cpp (revision 23102) +++ ps/trunk/source/gui/ObjectBases/IGUIObject.cpp (revision 23103) @@ -1,502 +1,515 @@ /* 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 "IGUIObject.h" #include "gui/CGUI.h" #include "gui/CGUISetting.h" #include "ps/CLogger.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" #include "soundmanager/ISoundManager.h" IGUIObject::IGUIObject(CGUI& pGUI) : m_pGUI(pGUI), m_pParent(), m_MouseHovering(), m_LastClickTime(), m_Enabled(), m_Hidden(), m_Size(), m_Style(), m_Hotkey(), m_Z(), m_Absolute(), m_Ghost(), m_AspectRatio(), m_Tooltip(), m_TooltipStyle() { RegisterSetting("enabled", m_Enabled); RegisterSetting("hidden", m_Hidden); RegisterSetting("size", m_Size); RegisterSetting("style", m_Style); RegisterSetting("hotkey", m_Hotkey); RegisterSetting("z", m_Z); RegisterSetting("absolute", m_Absolute); RegisterSetting("ghost", m_Ghost); RegisterSetting("aspectratio", m_AspectRatio); RegisterSetting("tooltip", m_Tooltip); RegisterSetting("tooltip_style", m_TooltipStyle); // Setup important defaults // TODO: Should be in the default style? 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); // m_Children is deleted along all other GUI Objects in the CGUI destructor } void IGUIObject::AddChild(IGUIObject& pChild) { pChild.SetParent(this); m_Children.push_back(&pChild); } template void IGUIObject::RegisterSetting(const CStr& Name, T& Value) { if (SettingExists(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, Value)); } bool IGUIObject::SettingExists(const CStr& Setting) const { return m_Settings.find(Setting) != m_Settings.end(); } 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.at(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.at(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 (m_Hidden) 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 == nullptr) { 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 && m_pParent->m_pParent == nullptr) return nullptr; return m_pParent; } void IGUIObject::ResetStates() { // Notify the gui that we aren't hovered anymore UpdateMouseOver(nullptr); } void IGUIObject::UpdateCachedSize() { // 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); else m_CachedActualSize = m_Size.GetSize(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 (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; } } } 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 { if (m_Absolute) return m_Z; 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& 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); } +void IGUIObject::UnsetScriptHandler(const CStr& Action) +{ + std::map >::iterator it = m_ScriptHandlers.find(Action); + + if (it == m_ScriptHandlers.end()) + return; + + m_ScriptHandlers.erase(it); + + if (m_ScriptHandlers.empty()) + JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetJSRuntime(), Trace, this); +} + 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); RegisterScriptFunctions(); } 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::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 (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::RegisterSetting(const CStr& Name, T& Value); \ 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/GUISettingTypes.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/GUISettingTypes.h" #undef GUITYPE_IGNORE_NONCOPYABLE #undef TYPE Index: ps/trunk/source/gui/ObjectBases/IGUIObject.h =================================================================== --- ps/trunk/source/gui/ObjectBases/IGUIObject.h (revision 23102) +++ ps/trunk/source/gui/ObjectBases/IGUIObject.h (revision 23103) @@ -1,496 +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 . */ /* * The base class of an object. * All objects are derived from this class. * It's an abstract data type, so it can't be used per se. * Also contains a Dummy object which is used for completely blank objects. */ #ifndef INCLUDED_IGUIOBJECT #define INCLUDED_IGUIOBJECT #include "gui/Scripting/JSInterface_IGUIObject.h" #include "gui/SettingTypes/CGUISize.h" #include "gui/SGUIMessage.h" #include "lib/input.h" // just for IN_PASS #include "ps/XML/Xeromyces.h" #include #include #include class CGUI; class IGUIObject; class IGUISetting; using map_pObjects = std::map; #define GUI_OBJECT(obj) \ public: \ static IGUIObject* ConstructObject(CGUI& pGUI) \ { return new obj(pGUI); } /** * GUI object such as a button or an input-box. * Abstract data type ! */ class IGUIObject { friend class CGUI; // Allow getProperty to access things like GetParent() friend bool JSI_IGUIObject::getProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp); friend bool JSI_IGUIObject::setProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp, JS::ObjectOpResult& result); + friend bool JSI_IGUIObject::deleteProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::ObjectOpResult& result); friend bool JSI_IGUIObject::getComputedSize(JSContext* cx, uint argc, JS::Value* vp); public: NONCOPYABLE(IGUIObject); IGUIObject(CGUI& pGUI); virtual ~IGUIObject(); /** * This function checks if the mouse is hovering the * rectangle that the base setting "size" makes. * Although it is virtual, so one could derive * an object from CButton, which changes only this * to checking the circle that "size" makes. * * This function also returns true if there is a different * GUI object shown on top of this one. */ virtual bool IsMouseOver() const; /** * This function returns true if the mouse is hovering * over this GUI object and if this GUI object is the * topmost object in that screen location. * For example when hovering dropdown list items, the * buttons beneath the list won't return true here. */ virtual bool IsMouseHovering() const { return m_MouseHovering; } /** * Test if mouse position is over an icon */ virtual bool MouseOverIcon(); //-------------------------------------------------------- /** @name Leaf Functions */ //-------------------------------------------------------- //@{ /// Get object name, name is unique const CStr& GetName() const { return m_Name; } /// Get object name void SetName(const CStr& Name) { m_Name = Name; } // Get Presentable name. // Will change all internally set names to something like "" CStr GetPresentableName() const; /** * Builds the object hierarchy with references. */ void AddChild(IGUIObject& pChild); /** * Return all child objects of the current object. */ const std::vector& GetChildren() const { return m_Children; } //@} //-------------------------------------------------------- /** @name Settings Management */ //-------------------------------------------------------- //@{ /** * Registers the given setting variables with the GUI object. * Enable XML and JS to modify the given variable. * * @param Type Setting type * @param Name Setting reference name */ template void RegisterSetting(const CStr& Name, T& Value); /** * Returns whether there is a setting with the given name registered. * * @param Setting setting name * @return True if settings exist. */ bool SettingExists(const CStr& Setting) const; /** * Get a mutable reference to the setting. * If no such setting exists, an exception of type std::out_of_range is thrown. * If the value is modified, there is no GUIM_SETTINGS_UPDATED message sent. */ template T& GetSetting(const CStr& Setting); template const T& GetSetting(const CStr& Setting) const; /** * Set a setting by string, regardless of what type it is. * Used to parse setting values from XML files. * For example a CRect(10,10,20,20) is created from "10 10 20 20". * Returns false if the conversion fails, otherwise true. */ bool SetSettingFromString(const CStr& Setting, const CStrW& Value, const bool SendMessage); /** * Assigns the given value to the setting identified by the given name. * Uses move semantics, so do not read from Value after this call. * * @param SendMessage If true, a GUIM_SETTINGS_UPDATED message will be broadcasted to all GUI objects. */ template void SetSetting(const CStr& Setting, T& Value, const bool SendMessage); /** * This variant will copy the value. */ template void SetSetting(const CStr& Setting, const T& Value, const bool SendMessage); /** * Returns whether this object is set to be hidden or ghost. */ bool IsEnabled() const; /** * Returns whether this is object is set to be hidden. */ bool IsHidden() const; /** * Returns whether this object is set to be hidden or ghost. */ bool IsHiddenOrGhost() const; /** * Retrieves the configured sound filename from the given setting name and plays that once. */ void PlaySound(const CStrW& soundPath) const; /** * Send event to this GUI object (HandleMessage and ScriptEvent) * * @param type Type of GUI message to be handled * @param EventName String representation of event name * @return IN_HANDLED if event was handled, or IN_PASS if skipped */ InReaction SendEvent(EGUIMessageType type, const CStr& EventName); /** * All sizes are relative to resolution, and the calculation * is not wanted in real time, therefore it is cached, update * the cached size with this function. */ virtual void UpdateCachedSize(); /** * Reset internal state of this object. */ virtual void ResetStates(); /** * Set the script handler for a particular object-specific action * * @param Action Name of action * @param Code Javascript code to execute when the action occurs * @param pGUI GUI instance to associate the script with */ void RegisterScriptHandler(const CStr& Action, const CStr& Code, CGUI& pGUI); /** * Inheriting classes may append JS functions to the JS object representing this class. */ virtual void RegisterScriptFunctions() {} /** * Retrieves the JSObject representing this GUI object. */ JSObject* GetJSObject(); //@} protected: //-------------------------------------------------------- /** @name Called by CGUI and friends * * Methods that the CGUI will call using * its friendship, these should not * be called by user. * These functions' security are a lot * what constitutes the GUI's */ //-------------------------------------------------------- //@{ public: /** * This function is called with different messages * for instance when the mouse enters the object. * * @param Message GUI Message */ virtual void HandleMessage(SGUIMessage& UNUSED(Message)) {} /** * Calls an IGUIObject member function recursively on this object and its children. * Aborts recursion at IGUIObjects that have the isRestricted function return true. * The arguments of the callback function must be references. */ template void RecurseObject(bool(IGUIObject::*isRestricted)() const, void(IGUIObject::*callbackFunction)(Args... args), Args&&... args) { if (!IsBaseObject()) { if (isRestricted && (this->*isRestricted)()) return; (this->*callbackFunction)(args...); } for (IGUIObject* const& obj : m_Children) obj->RecurseObject(isRestricted, callbackFunction, args...); } protected: /** * Draws the object. */ virtual void Draw() = 0; /** * Some objects need to handle the SDL_Event_ manually. * For instance the input box. * * Only the object with focus will have this function called. * * Returns either IN_PASS or IN_HANDLED. If IN_HANDLED, then * the key won't be passed on and processed by other handlers. * This is used for keys that the GUI uses. */ virtual InReaction ManuallyHandleEvent(const SDL_Event_* UNUSED(ev)) { return IN_PASS; } /** * Loads a style. */ void LoadStyle(const CStr& StyleName); /** * Returns not the Z value, but the actual buffered Z value, i.e. if it's * defined relative, then it will check its parent's Z value and add * the relativity. * * @return Actual Z value on the screen. */ virtual float GetBufferedZ() const; /** * Set parent of this object */ void SetParent(IGUIObject* pParent) { m_pParent = pParent; } public: CGUI& GetGUI() { return m_pGUI; } const CGUI& GetGUI() const { return m_pGUI; } /** * Take focus! */ void SetFocus(); protected: /** * Check if object is focused. */ bool IsFocused() const; /** * NOTE! This will not just return m_pParent, when that is * need use it! There is one exception to it, when the parent is * the top-node (the object that isn't a real object), this * will return nullptr, so that the top-node's children are * seemingly parentless. * * @return Pointer to parent */ IGUIObject* GetParent() const; /** * Handle additional children to the \-tag. In IGUIObject, this function does * nothing. In CList and CDropDown, it handles the \, used to build the data. * * Returning false means the object doesn't recognize the child. Should be reported. * Notice 'false' is default, because an object not using this function, should not * have any additional children (and this function should never be called). */ virtual bool HandleAdditionalChildren(const XMBElement& UNUSED(child), CXeromyces* UNUSED(pFile)) { return false; } /** * Allow the GUI object to process after all child items were handled. * Useful to avoid iterator invalidation with push_back calls. */ virtual void AdditionalChildrenHandled() {} /** * Cached size, real size m_Size is actually dependent on resolution * and can have different *real* outcomes, this is the real outcome * cached to avoid slow calculations in real time. */ CRect m_CachedActualSize; /** * Execute the script for a particular action. * Does nothing if no script has been registered for that action. * The mouse coordinates will be passed as the first argument. * * @param Action Name of action */ void ScriptEvent(const CStr& Action); /** * Execute the script for a particular action. * Does nothing if no script has been registered for that action. * * @param Action Name of action * @param paramData JS::HandleValueArray arguments to pass to the event. */ void ScriptEvent(const CStr& Action, const JS::HandleValueArray& paramData); + /** + * Assigns a JS function to the event name. + */ void SetScriptHandler(const CStr& Action, JS::HandleObject Function); /** + * Deletes an event handler assigned to the given name, if such a handler exists. + */ + void UnsetScriptHandler(const CStr& Action); + + /** * Inputes the object that is currently hovered, this function * updates this object accordingly (i.e. if it's the object * being inputted one thing happens, and not, another). * * @param pMouseOver Object that is currently hovered, can be nullptr too! */ void UpdateMouseOver(IGUIObject* const& pMouseOver); //@} private: //-------------------------------------------------------- /** @name Internal functions */ //-------------------------------------------------------- //@{ /** * Creates the JS object representing this page upon first use. */ void CreateJSObject(); /** * Updates some internal data depending on the setting changed. */ void PreSettingChange(const CStr& Setting); void SettingChanged(const CStr& Setting, const bool SendMessage); /** * Inputs a reference pointer, checks if the new inputted object * if hovered, if so, then check if this's Z value is greater * than the inputted object... If so then the object is closer * and we'll replace the pointer with this. * Also Notice input can be nullptr, which means the Z value demand * is out. NOTICE you can't input nullptr as const so you'll have * to set an object to nullptr. * * @param pObject Object pointer, can be either the old one, or * the new one. */ void ChooseMouseOverAndClosest(IGUIObject*& pObject); /** * Returns whether this is the object all other objects are descendants of. */ bool IsBaseObject() const; /** * Returns whether this object is a child of the base object. */ bool IsRootObject() const; static void Trace(JSTracer* trc, void* data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer* trc); // Variables protected: // Name of object CStr m_Name; // Constructed on the heap, will be destroyed along with the the CGUI std::vector m_Children; // Pointer to parent IGUIObject* m_pParent; //This represents the last click time for each mouse button double m_LastClickTime[6]; /** * This variable is true if the mouse is hovering this object and * this object is the topmost object shown in this location. */ bool m_MouseHovering; /** * Settings pool, all an object's settings are located here */ std::map m_Settings; // An object can't function stand alone CGUI& m_pGUI; // Internal storage for registered script handlers. std::map > m_ScriptHandlers; // Cached JSObject representing this GUI object JS::PersistentRootedObject m_JSObject; // Cache references to settings for performance bool m_Enabled; bool m_Hidden; CGUISize m_Size; CStr m_Style; CStr m_Hotkey; float m_Z; bool m_Absolute; bool m_Ghost; float m_AspectRatio; CStrW m_Tooltip; CStr m_TooltipStyle; }; #endif // INCLUDED_IGUIOBJECT Index: ps/trunk/source/gui/Scripting/JSInterface_IGUIObject.cpp =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_IGUIObject.cpp (revision 23102) +++ ps/trunk/source/gui/Scripting/JSInterface_IGUIObject.cpp (revision 23103) @@ -1,228 +1,257 @@ /* 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 "JSInterface_IGUIObject.h" #include "gui/CGUI.h" #include "gui/CGUISetting.h" #include "gui/ObjectBases/IGUIObject.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptInterface.h" JSClass JSI_IGUIObject::JSI_class = { "GUIObject", JSCLASS_HAS_PRIVATE, - nullptr, nullptr, - JSI_IGUIObject::getProperty, JSI_IGUIObject::setProperty, + nullptr, + JSI_IGUIObject::deleteProperty, + JSI_IGUIObject::getProperty, + JSI_IGUIObject::setProperty, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; JSFunctionSpec JSI_IGUIObject::JSI_methods[] = { JS_FN("toString", JSI_IGUIObject::toString, 0, 0), JS_FN("focus", JSI_IGUIObject::focus, 0, 0), JS_FN("blur", JSI_IGUIObject::blur, 0, 0), JS_FN("getComputedSize", JSI_IGUIObject::getComputedSize, 0, 0), JS_FS_END }; void JSI_IGUIObject::RegisterScriptClass(ScriptInterface& scriptInterface) { scriptInterface.DefineCustomObjectType(&JSI_class, nullptr, 0, nullptr, JSI_methods, nullptr, nullptr); } bool JSI_IGUIObject::getProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp) { JSAutoRequest rq(cx); ScriptInterface* pScriptInterface = ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface; IGUIObject* e = ScriptInterface::GetPrivate(cx, obj, &JSI_IGUIObject::JSI_class); if (!e) return false; JS::RootedValue idval(cx); if (!JS_IdToValue(cx, id, &idval)) return false; std::string propName; if (!ScriptInterface::FromJSVal(cx, idval, propName)) return false; // Skip registered functions and inherited properties // including JSInterfaces of derived classes if (propName == "constructor" || propName == "prototype" || propName == "toString" || propName == "toJSON" || propName == "focus" || propName == "blur" || propName == "getTextSize" || propName == "getComputedSize" ) return true; // Use onWhatever to access event handlers if (propName.substr(0, 2) == "on") { CStr eventName(CStr(propName.substr(2)).LowerCase()); std::map>::iterator it = e->m_ScriptHandlers.find(eventName); if (it == e->m_ScriptHandlers.end()) vp.setNull(); else vp.setObject(*it->second.get()); return true; } if (propName == "parent") { IGUIObject* parent = e->GetParent(); if (parent) vp.set(JS::ObjectValue(*parent->GetJSObject())); else vp.set(JS::NullValue()); return true; } else if (propName == "children") { ScriptInterface::CreateArray(cx, vp); for (size_t i = 0; i < e->m_Children.size(); ++i) pScriptInterface->SetPropertyInt(vp, i, e->m_Children[i]); return true; } else if (propName == "name") { ScriptInterface::ToJSVal(cx, vp, e->GetName()); return true; } else if (e->SettingExists(propName)) { e->m_Settings[propName]->ToJSVal(cx, vp); return true; } JS_ReportError(cx, "Property '%s' does not exist!", propName.c_str()); return false; } bool JSI_IGUIObject::setProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp, JS::ObjectOpResult& result) { IGUIObject* e = ScriptInterface::GetPrivate(cx, obj, &JSI_IGUIObject::JSI_class); if (!e) return result.fail(JSMSG_NOT_NONNULL_OBJECT); JSAutoRequest rq(cx); JS::RootedValue idval(cx); if (!JS_IdToValue(cx, id, &idval)) return result.fail(JSMSG_NOT_NONNULL_OBJECT); std::string propName; if (!ScriptInterface::FromJSVal(cx, idval, propName)) return result.fail(JSMSG_UNDEFINED_PROP); if (propName == "name") { std::string value; if (!ScriptInterface::FromJSVal(cx, vp, value)) return result.fail(JSMSG_UNDEFINED_PROP); e->SetName(value); return result.succeed(); } JS::RootedObject vpObj(cx); if (vp.isObject()) vpObj = &vp.toObject(); // Use onWhatever to set event handlers if (propName.substr(0, 2) == "on") { if (vp.isPrimitive() || vp.isNull() || !JS_ObjectIsFunction(cx, &vp.toObject())) { JS_ReportError(cx, "on- event-handlers must be functions"); return result.fail(JSMSG_NOT_FUNCTION); } CStr eventName(CStr(propName.substr(2)).LowerCase()); e->SetScriptHandler(eventName, vpObj); return result.succeed(); } if (e->SettingExists(propName)) return e->m_Settings[propName]->FromJSVal(cx, vp, true) ? result.succeed() : result.fail(JSMSG_TYPE_ERR_BAD_ARGS); JS_ReportError(cx, "Property '%s' does not exist!", propName.c_str()); return result.fail(JSMSG_UNDEFINED_PROP); } +bool JSI_IGUIObject::deleteProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::ObjectOpResult& result) +{ + IGUIObject* e = ScriptInterface::GetPrivate(cx, obj, &JSI_IGUIObject::JSI_class); + if (!e) + return result.fail(JSMSG_NOT_NONNULL_OBJECT); + + JSAutoRequest rq(cx); + JS::RootedValue idval(cx); + if (!JS_IdToValue(cx, id, &idval)) + return result.fail(JSMSG_NOT_NONNULL_OBJECT); + + std::string propName; + if (!ScriptInterface::FromJSVal(cx, idval, propName)) + return result.fail(JSMSG_UNDEFINED_PROP); + + // event handlers + if (propName.substr(0, 2) == "on") + { + CStr eventName(CStr(propName.substr(2)).LowerCase()); + e->UnsetScriptHandler(eventName); + return result.succeed(); + } + + JS_ReportError(cx, "Only event handlers can be deleted from GUI objects!"); + return result.fail(JSMSG_UNDEFINED_PROP); +} + bool JSI_IGUIObject::toString(JSContext* cx, uint argc, JS::Value* vp) { // No JSAutoRequest needed for these calls JS::CallArgs args = JS::CallArgsFromVp(argc, vp); IGUIObject* e = ScriptInterface::GetPrivate(cx, args, &JSI_IGUIObject::JSI_class); if (!e) return false; ScriptInterface::ToJSVal(cx, args.rval(), "[GUIObject: " + e->GetName() + "]"); return true; } bool JSI_IGUIObject::focus(JSContext* cx, uint argc, JS::Value* vp) { // No JSAutoRequest needed for these calls JS::CallArgs args = JS::CallArgsFromVp(argc, vp); IGUIObject* e = ScriptInterface::GetPrivate(cx, args, &JSI_IGUIObject::JSI_class); if (!e) return false; e->GetGUI().SetFocusedObject(e); args.rval().setUndefined(); return true; } bool JSI_IGUIObject::blur(JSContext* cx, uint argc, JS::Value* vp) { // No JSAutoRequest needed for these calls JS::CallArgs args = JS::CallArgsFromVp(argc, vp); IGUIObject* e = ScriptInterface::GetPrivate(cx, args, &JSI_IGUIObject::JSI_class); if (!e) return false; e->GetGUI().SetFocusedObject(nullptr); args.rval().setUndefined(); return true; } bool JSI_IGUIObject::getComputedSize(JSContext* cx, uint argc, JS::Value* vp) { JSAutoRequest rq(cx); JS::CallArgs args = JS::CallArgsFromVp(argc, vp); IGUIObject* e = ScriptInterface::GetPrivate(cx, args, &JSI_IGUIObject::JSI_class); if (!e) return false; e->UpdateCachedSize(); ScriptInterface::ToJSVal(cx, args.rval(), e->m_CachedActualSize); return true; } Index: ps/trunk/source/gui/Scripting/JSInterface_IGUIObject.h =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_IGUIObject.h (revision 23102) +++ ps/trunk/source/gui/Scripting/JSInterface_IGUIObject.h (revision 23103) @@ -1,39 +1,40 @@ /* 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 . */ #ifndef INCLUDED_JSI_IGUIOBJECT #define INCLUDED_JSI_IGUIOBJECT #include "scriptinterface/ScriptInterface.h" namespace JSI_IGUIObject { extern JSClass JSI_class; extern JSFunctionSpec JSI_methods[]; void RegisterScriptClass(ScriptInterface& scriptInterface); bool getProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp); bool setProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::MutableHandleValue vp, JS::ObjectOpResult& result); + bool deleteProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::ObjectOpResult& result); bool toString(JSContext* cx, uint argc, JS::Value* vp); bool focus(JSContext* cx, uint argc, JS::Value* vp); bool blur(JSContext* cx, uint argc, JS::Value* vp); bool getComputedSize(JSContext* cx, uint argc, JS::Value* vp); bool getTextSize(JSContext* cx, uint argc, JS::Value* vp); } #endif // INCLUDED_JSI_IGUIOBJECT