1
0
forked from 0ad/0ad

PushGuiPage support for passing a function instead of a function name.

Allows coding the GUI without global functions which break
prototype-oriented coding, refs #5322, fixing the concern in 4b1297b328.

Supports stacked message boxes and removes the according workaround.
Change structree / civinfo switch-dialog code from 760a47335d to perform
the callback for page that actually registered the callback.
Ensure the parent that the callbackhandler is always called if the page
is closed.
Merge PopGuiPage and PopGuiPageCB following that choice, incidentally
leaving cleaner code.

Differential Revision: https://code.wildfiregames.com/D1684
Comments by: Yves, Vladislav, wraitii, leper
This was SVN commit r22676.
This commit is contained in:
elexis 2019-08-16 18:46:04 +00:00
parent a06cfe309b
commit 86c151ebaa
32 changed files with 258 additions and 329 deletions

View File

@ -1,50 +1,18 @@
// We want to pass callback functions for the different buttons in a convenient way.
// Because passing functions accross compartment boundaries is a pain, we just store them here together with some optional arguments.
// The messageBox page will return the code of the pressed button and the according function will be called.
var g_MessageBoxBtnFunctions = [];
var g_MessageBoxCallbackArgs = [];
function messageBoxCallbackFunction(btnCode)
{
if (btnCode !== undefined && g_MessageBoxBtnFunctions[btnCode])
{
// Cache the variables to make it possible to call a messageBox from a callback function.
let callbackFunction = g_MessageBoxBtnFunctions[btnCode];
let callbackArgs = g_MessageBoxCallbackArgs[btnCode];
g_MessageBoxBtnFunctions = [];
g_MessageBoxCallbackArgs = [];
if (callbackArgs !== undefined)
callbackFunction(callbackArgs);
else
callbackFunction();
return;
}
g_MessageBoxBtnFunctions = [];
g_MessageBoxCallbackArgs = [];
}
function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs)
{
if (g_MessageBoxBtnFunctions && g_MessageBoxBtnFunctions.length)
{
warn("A messagebox was called when a previous callback function is still set, aborting!");
return;
}
g_MessageBoxBtnFunctions = mbBtnCode;
g_MessageBoxCallbackArgs = mbCallbackArgs || g_MessageBoxCallbackArgs;
Engine.PushGuiPage("page_msgbox.xml", {
"width": mbWidth,
"height": mbHeight,
"message": mbMessage,
"title": mbTitle,
"buttonCaptions": mbButtonCaptions,
"callback": mbBtnCode && "messageBoxCallbackFunction"
});
Engine.PushGuiPage(
"page_msgbox.xml",
{
"width": mbWidth,
"height": mbHeight,
"message": mbMessage,
"title": mbTitle,
"buttonCaptions": mbButtonCaptions
},
btnCode => {
if (mbBtnCode !== undefined && mbBtnCode[btnCode])
mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined);
});
}
function openURL(url)

View File

@ -7,24 +7,29 @@ function initTerms(terms)
function openTerms(page)
{
Engine.PushGuiPage("page_termsdialog.xml", {
"file": g_Terms[page].file,
"title": g_Terms[page].title,
"sprintf": g_Terms[page].sprintf,
"urlButtons": g_Terms[page].urlButtons || [],
"termsURL": g_Terms[page].termsURL || undefined,
"page": page,
"callback": "acceptTerms"
});
}
Engine.PushGuiPage(
"page_termsdialog.xml",
{
"file": g_Terms[page].file,
"title": g_Terms[page].title,
"sprintf": g_Terms[page].sprintf,
"urlButtons": g_Terms[page].urlButtons || [],
"termsURL": g_Terms[page].termsURL || undefined,
"page": page
},
data => {
g_Terms[data.page].accepted = data.accepted;
function acceptTerms(data)
{
g_Terms[data.page].accepted = data.accepted;
Engine.ConfigDB_CreateAndWriteValueToFile("user", g_Terms[data.page].config, data.accepted ? getTermsHash(data.page) : "0", "config/user.cfg");
Engine.ConfigDB_CreateAndWriteValueToFile(
"user",
g_Terms[data.page].config,
data.accepted ? getTermsHash(data.page) : "0",
"config/user.cfg");
if (g_Terms[data.page].callback)
g_Terms[data.page].callback(data);
if (g_Terms[data.page].callback)
g_Terms[data.page].callback(data);
}
);
}
function checkTerms()

View File

@ -268,9 +268,9 @@ function cancelRequest()
hideDialog();
}
function closePage(data)
function closePage()
{
Engine.PopGuiPageCB(undefined);
Engine.PopGuiPage();
}
function showErrorMessageBox(caption, title, buttonCaptions, buttonActions)

View File

@ -26,7 +26,5 @@ function downloadModsButton()
function openModIo(data)
{
if (data.accepted)
Engine.PushGuiPage("page_modio.xml", {
"callback": "initMods"
});
Engine.PushGuiPage("page_modio.xml", {}, initMods);
}

View File

@ -28,22 +28,15 @@ function init(data)
let mbButton = [];
captions.forEach((caption, i) => {
mbButton[i] = Engine.GetGUIObjectByName("mbButton" + (i + 1));
let action = function()
{
if (data.callback)
Engine.PopGuiPageCB(i);
else
Engine.PopGuiPage();
};
mbButton[i].caption = caption;
mbButton[i].onPress = action;
mbButton[i].hidden = false;
mbButton[i].onPress = () => {
Engine.PopGuiPage(i);
};
// Convention: Cancel is the first button
if (i == 0)
mbCancelHotkey.onPress = action;
mbCancelHotkey.onPress = mbButton[i].onPress;
});
// Distribute buttons horizontally

View File

@ -82,7 +82,7 @@ function initLanguageSelection()
function closeTerms(accepted)
{
Engine.PopGuiPageCB({
Engine.PopGuiPage({
"page": g_TermsPage,
"accepted": accepted
});

View File

@ -69,10 +69,7 @@ function checkBehavior()
function returnAI(save = true)
{
let idx = Engine.GetGUIObjectByName("aiSelection").selected;
// Pop the page before calling the callback, so the callback runs
// in the parent GUI page's context
Engine.PopGuiPageCB({
Engine.PopGuiPage({
"save": save,
"id": g_AIDescriptions[idx].id,
"name": g_AIDescriptions[idx].data.name,

View File

@ -3,11 +3,6 @@
*/
const g_CivData = loadCivData(true, false);
/**
* Callback function name on closing gui via Engine.PopGuiPage().
*/
var g_Callback = "";
var g_SelectedCiv = "";
/**
@ -15,9 +10,6 @@ var g_SelectedCiv = "";
*/
function init(data = {})
{
if (data.callback)
g_Callback = data.callback;
var civList = Object.keys(g_CivData).map(civ => ({ "name": g_CivData[civ].Name, "code": civ })).sort(sortNameIgnoreCase);
var civSelection = Engine.GetGUIObjectByName("civSelection");
@ -94,16 +86,12 @@ function subHeading(obj)
function switchToStrucTreePage()
{
Engine.PopGuiPage();
Engine.PushGuiPage("page_structree.xml", { "civ": g_SelectedCiv, "callback": g_Callback });
Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_structree.xml" });
}
function closePage()
{
if (g_Callback)
Engine.PopGuiPageCB({ "civ": g_SelectedCiv, "page": "page_civinfo.xml" });
else
Engine.PopGuiPage();
Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_civinfo.xml" });
}
/**

View File

@ -22,7 +22,7 @@ const g_CivData = loadCivData(false, false);
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
"code": "",
"civ": "",
"page": "page_civinfo.xml"
};
@ -1047,10 +1047,10 @@ var g_MiscControls = {
"hotkey_structree": colorizeHotkey("%(hotkey)s", "structree")
}),
"onPress": () => function() {
Engine.PushGuiPage(g_CivInfo.page, {
"civ": g_CivInfo.code,
"callback": "storeCivInfoPage"
});
Engine.PushGuiPage(
g_CivInfo.page,
{ "civ": g_CivInfo.civ },
storeCivInfoPage);
}
},
"civResetButton": {
@ -2423,30 +2423,26 @@ function openAIConfig(playerSlot)
{
g_LastViewedAIPlayer = playerSlot;
Engine.PushGuiPage("page_aiconfig.xml", {
"callback": "AIConfigCallback",
"playerSlot": playerSlot,
"id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
"difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
"behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
});
}
Engine.PushGuiPage(
"page_aiconfig.xml",
{
"playerSlot": playerSlot,
"id": g_GameAttributes.settings.PlayerData[playerSlot].AI,
"difficulty": g_GameAttributes.settings.PlayerData[playerSlot].AIDiff,
"behavior": g_GameAttributes.settings.PlayerData[playerSlot].AIBehavior
},
ai => {
g_LastViewedAIPlayer = -1;
/**
* Called after closing the dialog.
*/
function AIConfigCallback(ai)
{
g_LastViewedAIPlayer = -1;
if (!ai.save || !g_IsController)
return;
if (!ai.save || !g_IsController)
return;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty;
g_GameAttributes.settings.PlayerData[ai.playerSlot].AIBehavior = ai.behavior;
updateGameAttributes();
updateGameAttributes();
});
}
function reloadPlayerAssignmentChoices()
@ -2775,6 +2771,11 @@ function updateAutocompleteEntries()
function storeCivInfoPage(data)
{
g_CivInfo.code = data.civ;
g_CivInfo.page = data.page;
if (data.nextPage)
Engine.PushGuiPage(
data.nextPage,
{ "civ": data.civ },
storeCivInfoPage);
else
g_CivInfo = data;
}

View File

@ -6,15 +6,11 @@
<script directory="gui/gamesetup/"/>
<object hotkey="civinfo">
<action on="Press">
Engine.PushGuiPage("page_civinfo.xml", { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" });
</action>
<action on="Press">Engine.PushGuiPage("page_civinfo.xml", { "civ": g_CivInfo.civ }, storeCivInfoPage);</action>
</object>
<object hotkey="structree">
<action on="Press">
Engine.PushGuiPage("page_structree.xml", { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" });
</action>
<action on="Press">Engine.PushGuiPage("page_structree.xml", { "civ": g_CivInfo.civ }, storeCivInfoPage);</action>
</object>
<object hotkey="cancel">

View File

@ -55,7 +55,7 @@ function languageSelectionChanged()
function openAdvancedMenu()
{
let localeText = Engine.GetGUIObjectByName("localeText");
Engine.PushGuiPage("page_locale_advanced.xml", { "callback": "applyFromAdvancedMenu", "locale": localeText.caption });
Engine.PushGuiPage("page_locale_advanced.xml", { "locale": localeText.caption }, applyFromAdvancedMenu);
}
function applyFromAdvancedMenu(locale)

View File

@ -114,5 +114,5 @@ function autoDetectLocale()
function applySelectedLocale()
{
var resultingLocaleText = Engine.GetGUIObjectByName("resultingLocale");
Engine.PopGuiPageCB(resultingLocaleText.caption);
Engine.PopGuiPage(resultingLocaleText.caption);
}

View File

@ -1,15 +0,0 @@
var hasCallback = false;
function init(data)
{
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile("gui/manual/intro.txt"));
hasCallback = data && data.callback;
}
function closeManual()
{
if (hasCallback)
Engine.PopGuiPageCB();
else
Engine.PopGuiPage();
}

View File

@ -14,13 +14,16 @@
</object>
<object type="image" sprite="ModernFade" size="20 20 100%-20 100%-58">
<object name="mainText" type="text" style="ModernTextPanel"/>
<object name="mainText" type="text" style="ModernTextPanel">
<action on="Load">this.caption = Engine.TranslateLines(Engine.ReadFile("gui/manual/intro.txt"));</action>
</object>
</object>
<object type="button" style="ModernButtonRed" tooltip_style="snToolTip" size="100%-408 100%-52 100%-218 100%-24" hotkey="cancel">
<translatableAttribute id="caption">Close</translatableAttribute>
<action on="Press">closeManual();</action>
<action on="Press">Engine.PopGuiPage();</action>
</object>
<object name="url" type="button" style="ModernButtonRed" size="100%-214 100%-52 100%-24 100%-24">
<translatableAttribute id="caption">View Online</translatableAttribute>
<action on="Press">openURL("https://trac.wildfiregames.com/wiki/0adManual");</action>

View File

@ -4,12 +4,7 @@
var g_Options;
/**
* Remember whether to unpause running singleplayer games.
*/
var g_HasCallback;
/**
* Functions to call after closing the page.
* Names of session functions to be called after closing the page.
*/
var g_CloseCallbacks;
@ -170,8 +165,7 @@ var g_OptionType = {
function init(data, hotloadData)
{
g_CloseCallbacks = new Set();
g_HasCallback = hotloadData && hotloadData.callback || data && data.callback;
g_CloseCallbacks = hotloadData ? hotloadData.closeCallbacks : new Set();
g_TabCategorySelected = hotloadData ? hotloadData.tabCategorySelected : 0;
g_Options = Engine.ReadJSONFile("gui/options/options.json");
@ -190,7 +184,7 @@ function getHotloadData()
{
return {
"tabCategorySelected": g_TabCategorySelected,
"callback": g_HasCallback
"closeCallbacks": g_CloseCallbacks
};
}
@ -382,8 +376,5 @@ function closePage()
function closePageWithoutConfirmation()
{
if (g_HasCallback)
Engine.PopGuiPageCB(g_CloseCallbacks);
else
Engine.PopGuiPage();
Engine.PopGuiPage(g_CloseCallbacks);
}

View File

@ -105,9 +105,7 @@ function onTick()
function ShowSplashScreen()
{
Engine.PushGuiPage("page_splashscreen.xml", {
"callback": "ShowRenderPathMessage"
});
Engine.PushGuiPage("page_splashscreen.xml", {}, ShowRenderPathMessage);
}
function ShowRenderPathMessage()
@ -232,3 +230,15 @@ function getLobbyDisabledByBuild()
{
return translate("Launch the multiplayer lobby to join and host publicly visible games and chat with other players. \\[DISABLED BY BUILD]");
}
function openStrucTree(page)
{
closeMenu();
Engine.PushGuiPage(page, {}, storeCivInfoPage);
}
function storeCivInfoPage(data)
{
if (data.nextPage)
Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, storeCivInfoPage);
}

View File

@ -118,10 +118,7 @@
hotkey="structree"
>
<translatableAttribute id="caption">Structure Tree</translatableAttribute>
<action on="Press">
closeMenu();
Engine.PushGuiPage("page_structree.xml", {});
</action>
<action on="Press">openStrucTree("page_structree.xml");</action>
</object>
<!-- HISTORY BUTTON -->
@ -134,10 +131,7 @@
hotkey="civinfo"
>
<translatableAttribute id="caption">History</translatableAttribute>
<action on="Press">
closeMenu();
Engine.PushGuiPage("page_civinfo.xml");
</action>
<action on="Press">openStrucTree("page_civinfo.xml");</action>
</object>
</object>

View File

@ -1,13 +1,4 @@
var g_SelectedCiv = "gaia";
var g_CallbackSet = false;
function closePage()
{
if (g_CallbackSet)
Engine.PopGuiPageCB(0);
else
Engine.PopGuiPage();
}
/**
* Compile lists of templates buildable/trainable/researchable of a given civ.

View File

@ -8,11 +8,6 @@ var g_BuildList = {};
*/
var g_TrainList = {};
/**
* Callback function name on closing gui via Engine.PopGuiPage().
*/
var g_Callback = "";
/**
* Initialize the page
*
@ -20,9 +15,6 @@ var g_Callback = "";
*/
function init(data = {})
{
if (data.callback)
g_Callback = data.callback;
let civList = Object.keys(g_CivData).map(civ => ({
"name": g_CivData[civ].Name,
"code": civ,
@ -52,16 +44,12 @@ function init(data = {})
function switchToCivInfoPage()
{
Engine.PopGuiPage();
Engine.PushGuiPage("page_civinfo.xml", { "civ": g_SelectedCiv, "callback": g_Callback });
Engine.PopGuiPage({ "civ": g_SelectedCiv, "nextPage": "page_civinfo.xml" });
}
function closePage()
{
if (g_Callback)
Engine.PopGuiPageCB({ "civ": g_SelectedCiv, "page": "page_structree.xml" });
else
Engine.PopGuiPage();
Engine.PopGuiPage({ "civ": g_SelectedCiv, "page": "page_structree.xml" });
}
/**

View File

@ -47,9 +47,6 @@ var g_RankIconPath = "session/icons/ranks/";
* @param {object} data - Contains the civCode and the name of the template to display.
* @param {string} data.templateName
* @param {string} [data.civ]
* @param {*} [data.callback] - If set and loosely equivalent to true, a callback is
* assumed to be setup ready be called by the Engine upon
* closure of this page.
*/
function init(data)
{
@ -60,9 +57,6 @@ function init(data)
return;
}
if (data.callback)
g_CallbackSet = true;
let templateName = removeFiltersFromTemplateName(data.templateName);
let isTech = techDataExists(templateName);

View File

@ -35,7 +35,7 @@
hotkey="cancel"
>
<translatableAttribute id="caption">Close</translatableAttribute>
<action on="Press">closePage();</action>
<action on="Press">Engine.PopGuiPage();</action>
</object>
</object>

View File

@ -80,7 +80,7 @@ function reallySaveGame(name, desc, nameIsPrefix)
function closeSave()
{
Engine.PopGuiPageCB(0);
Engine.PopGuiPage();
}
// HACK: Engine.SaveGame* expects this function to be defined on the current page.

View File

@ -33,15 +33,11 @@
</object>
<object hotkey="civinfo">
<action on="Press">
Engine.PushGuiPage("page_civinfo.xml", { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" });
</action>
<action on="Press">openStrucTree("page_civinfo.xml");</action>
</object>
<object hotkey="structree">
<action on="Press">
Engine.PushGuiPage("page_structree.xml", { "civ": g_CivInfo.code, "callback": "storeCivInfoPage" });
</action>
<action on="Press">openStrucTree("page_structree.xml");</action>
</object>
<object hotkey="silhouettes">

View File

@ -29,7 +29,7 @@ var g_IdleTraderTextColor = "orange";
* Store civilization code and page (structree or history) opened in civilization info.
*/
var g_CivInfo = {
"code": "",
"civ": "",
"page": "page_structree.xml"
};
@ -225,10 +225,10 @@ function openSave()
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_savegame.xml", {
"savedGameData": getSavedGameData(),
"callback": "resumeGame"
});
Engine.PushGuiPage(
"page_savegame.xml",
{ "savedGameData": getSavedGameData() },
resumeGame);
}
function openOptions()
@ -236,18 +236,16 @@ function openOptions()
closeOpenDialogs();
pauseGame();
Engine.PushGuiPage("page_options.xml", {
"callback": "optionsPageClosed"
});
}
Engine.PushGuiPage(
"page_options.xml",
{},
callbackFunctionNames => {
for (let functionName of callbackFunctionNames)
if (global[functionName])
global[functionName]();
function optionsPageClosed(data)
{
for (let callback of data)
if (global[callback])
global[callback]();
resumeGame();
resumeGame();
});
}
function openChat(command = "")
@ -1090,41 +1088,51 @@ function openGameSummary()
pauseGame();
let extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
Engine.PushGuiPage("page_summary.xml", {
"sim": {
"mapSettings": g_GameAttributes.settings,
"playerStates": extendedSimState.players.filter((state, player) =>
g_IsObserver || player == 0 || player == g_ViewedPlayer ||
extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
"timeElapsed": extendedSimState.timeElapsed
Engine.PushGuiPage(
"page_summary.xml",
{
"sim": {
"mapSettings": g_GameAttributes.settings,
"playerStates": extendedSimState.players.filter((state, player) =>
g_IsObserver || player == 0 || player == g_ViewedPlayer ||
extendedSimState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
"timeElapsed": extendedSimState.timeElapsed
},
"gui": {
"dialog": true,
"isInGame": true
},
"selectedData": g_SummarySelectedData
},
"gui": {
"dialog": true,
"isInGame": true
},
"selectedData": g_SummarySelectedData,
"callback": "resumeGameAndSaveSummarySelectedData"
});
resumeGameAndSaveSummarySelectedData);
}
function openStrucTree()
function openStrucTree(page)
{
closeOpenDialogs();
pauseGame();
// TODO add info about researched techs and unlocked entities
Engine.PushGuiPage(g_CivInfo.page, {
"civ": g_CivInfo.code || g_Players[g_ViewedPlayer].civ,
"callback": "storeCivInfoPage"
});
Engine.PushGuiPage(
page,
{
"civ": g_CivInfo.civ || g_Players[g_ViewedPlayer].civ
// TODO add info about researched techs and unlocked entities
},
storeCivInfoPage);
}
function storeCivInfoPage(data)
{
g_CivInfo.code = data.civ;
g_CivInfo.page = data.page;
resumeGame();
if (data.nextPage)
Engine.PushGuiPage(
data.nextPage,
{ "civ": data.civ },
storeCivInfoPage);
else
{
g_CivInfo = data;
resumeGame();
}
}
/**

View File

@ -1143,11 +1143,13 @@ function showTemplateDetails(templateName, civCode)
{
pauseGame();
Engine.PushGuiPage("page_viewer.xml", {
"templateName": templateName,
"callback": "resumeGame",
"civ": civCode
});
Engine.PushGuiPage(
"page_viewer.xml",
{
"templateName": templateName,
"civ": civCode
},
resumeGame);
}
/**

View File

@ -6,6 +6,6 @@
tooltip_style="sessionToolTipBold"
>
<object name="civIconOverlay" style="CivIconOverlay" size="0 0 100% 100%" type="button">
<action on="Press">openStrucTree()</action>
<action on="Press">openStrucTree(g_CivInfo.page)</action>
</object>
</object>

View File

@ -10,5 +10,5 @@ function closePage()
{
Engine.ConfigDB_CreateAndWriteValueToFile("user", "gui.splashscreen.enable", String(Engine.GetGUIObjectByName("displaySplashScreen").checked), "config/user.cfg");
Engine.ConfigDB_CreateAndWriteValueToFile("user", "gui.splashscreen.version", Engine.GetFileMTime(g_SplashScreenFile), "config/user.cfg");
Engine.PopGuiPageCB();
Engine.PopGuiPage();
}

View File

@ -443,7 +443,7 @@ function continueButton()
"charts": g_SelectedChart
};
if (g_GameData.gui.isInGame)
Engine.PopGuiPageCB({
Engine.PopGuiPage({
"explicitResume": 0,
"summarySelectedData": summarySelectedData
});

View File

@ -89,19 +89,24 @@ void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptIn
m_PageStack.clear();
PushPage(pageName, initDataClone);
PushPage(pageName, initDataClone, JS::UndefinedHandleValue);
}
void CGUIManager::PushPage(const CStrW& pageName, shared_ptr<ScriptInterface::StructuredClone> initData)
void CGUIManager::PushPage(const CStrW& pageName, shared_ptr<ScriptInterface::StructuredClone> initData, JS::HandleValue callbackFunction)
{
// Store the callback handler in the current GUI page before opening the new one
if (!m_PageStack.empty() && !callbackFunction.isUndefined())
m_PageStack.back().SetCallbackFunction(*m_ScriptInterface, callbackFunction);
// Push the page prior to loading its contents, because that may push
// another GUI page on init which should be pushed on top of this new page.
m_PageStack.emplace_back(pageName, initData);
m_PageStack.back().LoadPage(m_ScriptRuntime);
ResetCursor();
}
void CGUIManager::PopPage()
void CGUIManager::PopPage(shared_ptr<ScriptInterface::StructuredClone> args)
{
if (m_PageStack.size() < 2)
{
@ -110,58 +115,11 @@ void CGUIManager::PopPage()
}
m_PageStack.pop_back();
}
void CGUIManager::PopPageCB(shared_ptr<ScriptInterface::StructuredClone> args)
{
shared_ptr<ScriptInterface::StructuredClone> initDataClone = m_PageStack.back().initData;
PopPage();
shared_ptr<ScriptInterface> scriptInterface = m_PageStack.back().gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue initDataVal(cx);
if (!initDataClone)
{
LOGERROR("Called PopPageCB when initData (which should contain the callback function name) isn't set!");
return;
}
scriptInterface->ReadStructuredClone(initDataClone, &initDataVal);
if (!scriptInterface->HasProperty(initDataVal, "callback"))
{
LOGERROR("Called PopPageCB when the callback function name isn't set!");
return;
}
std::string callback;
if (!scriptInterface->GetProperty(initDataVal, "callback", callback))
{
LOGERROR("Failed to get the callback property as a string from initData in PopPageCB!");
return;
}
JS::RootedValue global(cx, scriptInterface->GetGlobalObject());
if (!scriptInterface->HasProperty(global, callback.c_str()))
{
LOGERROR("The specified callback function %s does not exist in the page %s", callback, utf8_from_wstring(m_PageStack.back().name));
return;
}
JS::RootedValue argVal(cx);
if (args)
scriptInterface->ReadStructuredClone(args, &argVal);
if (!scriptInterface->CallFunctionVoid(global, callback.c_str(), argVal))
{
LOGERROR("Failed to call the callback function %s in the page %s", callback, utf8_from_wstring(m_PageStack.back().name));
return;
}
m_PageStack.back().PerformCallbackFunction(args);
}
CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const shared_ptr<ScriptInterface::StructuredClone> initData)
: name(pageName), initData(initData), inputs(), gui()
: name(pageName), initData(initData), inputs(), gui(), callbackFunction()
{
}
@ -256,6 +214,52 @@ void CGUIManager::SGUIPage::LoadPage(shared_ptr<ScriptRuntime> scriptRuntime)
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(name));
}
void CGUIManager::SGUIPage::SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc)
{
if (!callbackFunc.isObject())
{
LOGERROR("Given callback handler is not an object!");
return;
}
// Does not require JSAutoRequest
if (!JS_ObjectIsFunction(scriptInterface.GetContext(), &callbackFunc.toObject()))
{
LOGERROR("Given callback handler is not a function!");
return;
}
callbackFunction = std::make_shared<JS::PersistentRootedValue>(scriptInterface.GetJSRuntime(), callbackFunc);
}
void CGUIManager::SGUIPage::PerformCallbackFunction(shared_ptr<ScriptInterface::StructuredClone> args)
{
if (!callbackFunction)
return;
shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
JSContext* cx = scriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedObject globalObj(cx, &scriptInterface->GetGlobalObject().toObject());
JS::RootedValue funcVal(cx, *callbackFunction);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
JS::RootedValue argVal(cx);
if (args)
scriptInterface->ReadStructuredClone(args, &argVal);
JS::AutoValueVector paramData(cx);
paramData.append(argVal);
JS::RootedValue result(cx);
JS_CallFunctionValue(cx, globalObj, funcVal, paramData, &result);
}
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
{
for (SGUIPage& p : m_PageStack)

View File

@ -72,15 +72,15 @@ public:
* Load a new GUI page and make it active. All current pages will be retained,
* and will still be drawn and receive tick events, but will not receive
* user inputs.
* If given, the callbackHandler function will be executed once this page is closed.
*/
void PushPage(const CStrW& pageName, shared_ptr<ScriptInterface::StructuredClone> initData);
void PushPage(const CStrW& pageName, shared_ptr<ScriptInterface::StructuredClone> initData, JS::HandleValue callbackFunc);
/**
* Unload the currently active GUI page, and make the previous page active.
* (There must be at least two pages when you call this.)
*/
void PopPage();
void PopPageCB(shared_ptr<ScriptInterface::StructuredClone> args);
void PopPage(shared_ptr<ScriptInterface::StructuredClone> args);
/**
* Called when a file has been modified, to hotload changes.
@ -145,13 +145,37 @@ private:
{
// COPYABLE, because event handlers may invalidate page stack iterators by open or close pages,
// and event handlers need to be called for the entire stack.
/**
* Initializes the data that will be used to create the CGUI page one or multiple times (hotloading).
*/
SGUIPage(const CStrW& pageName, const shared_ptr<ScriptInterface::StructuredClone> initData);
/**
* Create the CGUI with it's own ScriptInterface. Deletes the previous CGUI if it existed.
*/
void LoadPage(shared_ptr<ScriptRuntime> scriptRuntime);
/**
* Sets the callback handler when a new page is opened that will be performed when the page is closed.
*/
void SetCallbackFunction(ScriptInterface& scriptInterface, JS::HandleValue callbackFunc);
/**
* Execute the stored callback function with the given arguments.
*/
void PerformCallbackFunction(shared_ptr<ScriptInterface::StructuredClone> args);
CStrW name;
boost::unordered_set<VfsPath> inputs; // for hotloading
shared_ptr<ScriptInterface::StructuredClone> initData; // data to be passed to the init() function
shared_ptr<CGUI> gui; // the actual GUI page
/**
* Function executed by this parent GUI page when the child GUI page it pushed is popped.
* Notice that storing it in the SGUIPage instead of CGUI means that it will survive the hotloading CGUI reset.
*/
shared_ptr<JS::PersistentRootedValue> callbackFunction;
};
shared_ptr<CGUI> top() const;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2018 Wildfire Games.
/* 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
@ -27,9 +27,9 @@
// Note that the initData argument may only contain clonable data.
// Functions aren't supported for example!
void JSI_GUIManager::PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData)
void JSI_GUIManager::PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction)
{
g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData));
g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData), callbackFunction);
}
void JSI_GUIManager::SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData)
@ -37,14 +37,9 @@ void JSI_GUIManager::SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const
g_GUI->SwitchPage(name, pCxPrivate->pScriptInterface, initData);
}
void JSI_GUIManager::PopGuiPage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
void JSI_GUIManager::PopGuiPage(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args)
{
g_GUI->PopPage();
}
void JSI_GUIManager::PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args)
{
g_GUI->PopPageCB(pCxPrivate->pScriptInterface->WriteStructuredClone(args));
g_GUI->PopPage(pCxPrivate->pScriptInterface->WriteStructuredClone(args));
}
JS::Value JSI_GUIManager::GetGUIObjectByName(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name)
@ -82,10 +77,9 @@ CParamNode JSI_GUIManager::GetTemplate(ScriptInterface::CxPrivate* UNUSED(pCxPri
void JSI_GUIManager::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<void, std::wstring, JS::HandleValue, &PushGuiPage>("PushGuiPage");
scriptInterface.RegisterFunction<void, std::wstring, JS::HandleValue, JS::HandleValue, &PushGuiPage>("PushGuiPage");
scriptInterface.RegisterFunction<void, std::wstring, JS::HandleValue, &SwitchGuiPage>("SwitchGuiPage");
scriptInterface.RegisterFunction<void, &PopGuiPage>("PopGuiPage");
scriptInterface.RegisterFunction<void, JS::HandleValue, &PopGuiPageCB>("PopGuiPageCB");
scriptInterface.RegisterFunction<void, JS::HandleValue, &PopGuiPage>("PopGuiPage");
scriptInterface.RegisterFunction<JS::Value, std::string, &GetGUIObjectByName>("GetGUIObjectByName");
scriptInterface.RegisterFunction<std::wstring, std::wstring, &SetCursor>("SetCursor");
scriptInterface.RegisterFunction<void, &ResetCursor>("ResetCursor");

View File

@ -23,10 +23,9 @@
namespace JSI_GUIManager
{
void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData);
void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction);
void SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData);
void PopGuiPage(ScriptInterface::CxPrivate* pCxPrivate);
void PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args);
void PopGuiPage(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args);
JS::Value GetGUIObjectByName(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name);
std::wstring SetCursor(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name);
void ResetCursor(ScriptInterface::CxPrivate* pCxPrivate);