1
0
forked from 0ad/0ad

Compare commits

...

4 Commits

70 changed files with 406 additions and 290 deletions

View File

@ -0,0 +1,4 @@
function init()
{
return new Promise(resolve => global.closePage = resolve);
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/regainFocus/emptyPage.js"/>
</objects>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/styles.xml</include>
<include>regainFocus/emptyPage.xml</include>
</page>

View File

@ -1 +1,4 @@
Engine.PushGuiPage("regainFocus/page_emptyPage.xml").then(Engine.PopGuiPage);
function init()
{
return Engine.PushGuiPage("regainFocus/page_emptyPage.xml");
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/styles.xml</include>
<include>resolveReject/resolveReject.xml</include>
</page>

View File

@ -0,0 +1,5 @@
async function init(reject)
{
if (reject)
throw undefined;
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/resolveReject/resolveReject.js"/>
</objects>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/styles.xml</include>
<include>sequential/sequential.xml</include>
</page>

View File

@ -0,0 +1,5 @@
async function init()
{
await Engine.PushGuiPage("regainFocus/page_emptyPage.xml");
await Engine.PushGuiPage("regainFocus/page_emptyPage.xml");
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script file="gui/sequential/sequential.js"/>
</objects>

View File

@ -7,7 +7,7 @@ class ColorMixer
/**
* @param {String} color - initial color as RGB string e.g. 100 0 200
*/
constructor(color)
constructor()
{
this.panel = Engine.GetGUIObjectByName("main");
this.colorDisplay = Engine.GetGUIObjectByName("colorDisplay");
@ -15,8 +15,6 @@ class ColorMixer
this.color = [0, 0, 0];
this.sliders = [];
this.valuesText = [];
this.setup(color);
}
async setup(color)
@ -68,11 +66,7 @@ class ColorMixer
// Update return color on cancel to prevent malformed values from initial input.
color = this.color.join(" ");
const buttonIndex = await closePromise;
if (buttonIndex === 0)
Engine.PopGuiPage(color);
else
Engine.PopGuiPage(this.color.join(" "));
return await closePromise === 0 ? color : this.color.join(" ");
}
updateFromSlider(index)
@ -95,5 +89,5 @@ ColorMixer.prototype.captions = [translate("Cancel"), translate("Save")];
function init(color)
{
new ColorMixer(color);
return new ColorMixer().setup(color);
}

View File

@ -3,9 +3,5 @@ var g_IncompatibleModsFile = "gui/incompatible_mods/incompatible_mods.txt";
function init(data)
{
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile(g_IncompatibleModsFile));
}
function closePage()
{
Engine.PopGuiPage();
return new Promise(popGuiPage => { Engine.GetGUIObjectByName("btnClose").onPress = popGuiPage; });
}

View File

@ -17,7 +17,6 @@
<object name="btnClose" type="button" style="ModernButtonRed" size="18 100%-45 100%-18 100%-17" hotkey="cancel">
<translatableAttribute id="caption">Ok</translatableAttribute>
<action on="Press">closePage();</action>
</object>
</object>

View File

@ -27,10 +27,10 @@ var g_ModIOState = {
/**
* Finished status indicators
*/
"ready": progressData => {
"ready": (progressData, popGuiPage) => {
// GameID acquired, ready to fetch mod list
if (!g_RequestCancelled)
updateModList();
updateModList(popGuiPage);
},
"listed": progressData => {
// List of available mods acquired
@ -65,7 +65,7 @@ var g_ModIOState = {
/**
* Error/Failure status indicators.
*/
"failed_gameid": async(progressData) => {
"failed_gameid": async(progressData, popGuiPage) => {
// Game ID couldn't be retrieved
const promise = showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Game ID could not be retrieved.\n\n%(technicalDetails)s"), {
@ -76,11 +76,11 @@ var g_ModIOState = {
if (!promise)
return;
if (await promise === 0)
closePage();
popGuiPage();
else
init();
},
"failed_listing": async(progressData) => {
"failed_listing": async(progressData, popGuiPage) => {
// Mod list couldn't be retrieved
const promise = showErrorMessageBox(
sprintf(translateWithContext("mod.io error message", "Mod List could not be retrieved.\n\n%(technicalDetails)s"), {
@ -91,9 +91,9 @@ var g_ModIOState = {
if (!promise)
return;
if (await promise === 0)
cancelModListUpdate();
cancelModListUpdate(popGuiPage);
else
updateModList();
updateModList(popGuiPage);
},
"failed_downloading": async(progressData) => {
// File couldn't be retrieved
@ -131,7 +131,7 @@ var g_ModIOState = {
}
};
async function init(data)
function init(data)
{
const promise = progressDialog(
translate("Initializing mod.io interface."),
@ -142,11 +142,16 @@ async function init(data)
g_Failure = false;
Engine.ModIoStartGetGameId();
await promise;
closePage();
return Promise.race([
promise,
new Promise(popGuiPage => {
Engine.GetGUIObjectByName("backButton").onPress = popGuiPage;
Engine.GetGUIObjectByName("modio").onTick = onTick.bind(null, popGuiPage);
});
};
}
function onTick()
function onTick(popGuiPage)
{
let progressData = Engine.ModIoGetDownloadProgress();
@ -157,7 +162,7 @@ function onTick()
return;
}
handler(progressData);
handler(progressData, popGuiPage);
if (!progressData.status.startsWith("failed"))
Engine.ModIoAdvanceRequest();
}
@ -230,13 +235,13 @@ function showModDescription()
Engine.GetGUIObjectByName("modError").caption = isSelected && isInvalid ? sprintf(translate("Invalid mod: %(error)s"), {"error": g_ModsAvailableOnline[selected].error }) : "";
}
function cancelModListUpdate()
function cancelModListUpdate(popGuiPage)
{
cancelRequest();
if (!g_ModsAvailableOnline.length)
{
closePage();
popGuiPage();
return;
}
@ -244,7 +249,7 @@ function cancelModListUpdate()
Engine.GetGUIObjectByName('refreshButton').enabled = true;
}
async function updateModList()
async function updateModList(popGuiPage)
{
clearModList();
Engine.GetGUIObjectByName("refreshButton").enabled = false;
@ -260,7 +265,7 @@ async function updateModList()
Engine.ModIoStartListMods();
await promise;
cancelModListUpdate();
cancelModListUpdate(popGuiPage);
}
async function downloadMod()
@ -296,11 +301,6 @@ function cancelRequest()
hideDialog();
}
function closePage()
{
Engine.PopGuiPage();
}
function showErrorMessageBox(caption, title, buttonCaptions)
{
if (g_Failure)

View File

@ -8,11 +8,6 @@
<object type="image" sprite="ModernFade"/>
<object name="modio" type="image" style="ModernDialog" size="10% 10% 90% 90%">
<action on="Tick">
onTick();
</action>
<!-- Page Title -->
<object style="ModernLabelText" type="text" size="50%-128 -18 50%+128 14">
<translatableAttribute id="caption">mod.io Mods</translatableAttribute>
@ -84,9 +79,8 @@
</object>
<!-- Buttons -->
<object type="button" style="ModernButtonRed" size="100%-552 100%-44 100%-372 100%-16">
<object name="backButton" type="button" style="ModernButtonRed" size="100%-552 100%-44 100%-372 100%-16">
<translatableAttribute id="caption">Back</translatableAttribute>
<action on="Press">closePage();</action>
</object>
<object name="refreshButton" type="button" style="ModernButtonRed" size="100%-368 100%-44 100%-188 100%-16" enabled="false">

View File

@ -1,4 +1,5 @@
function init(data)
{
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile("gui/modmod/help/help.txt"));
return new Promise(popGuiPage => { Engine.GetGUIObjectByName("closeButton").onPress = popGuiPage; });
}

View File

@ -17,9 +17,8 @@
<object name="mainText" type="text" style="ModernTextPanel"/>
</object>
<object type="button" style="ModernButtonRed" tooltip_style="snToolTip" size="100%-602 100%-52 100%-412 100%-24" hotkey="cancel">
<object name="closeButton" type="button" style="ModernButtonRed" tooltip_style="snToolTip" size="100%-602 100%-52 100%-412 100%-24" hotkey="cancel">
<translatableAttribute id="caption">Close</translatableAttribute>
<action on="Press">Engine.PopGuiPage();</action>
</object>
<object type="button" style="ModernButtonRed" size="100%-408 100%-52 100%-218 100%-24">
<translatableAttribute id="caption">Modding Guide</translatableAttribute>

View File

@ -2,7 +2,7 @@
* Currently limited to at most 3 buttons per message box.
* The convention is to have "cancel" appear first.
*/
async function init(data)
function init(data)
{
// Set title
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
@ -15,7 +15,6 @@ async function init(data)
// Default behaviour
let mbCancelHotkey = Engine.GetGUIObjectByName("mbCancelHotkey");
mbCancelHotkey.onPress = Engine.PopGuiPage;
// Calculate size
let mbLRDiff = data.width / 2;
@ -28,5 +27,5 @@ async function init(data)
const closePromise = setButtonCaptionsAndVisibitily(mbButton, captions, mbCancelHotkey, "mbButton");
distributeButtonsHorizontally(mbButton, captions);
Engine.PopGuiPage(await closePromise);
return closePromise;
}

View File

@ -11,7 +11,7 @@ var g_TermsPage;
var g_TermsFile;
var g_TermsSprintf;
function init(data)
async function init(data)
{
g_TermsPage = data.page;
g_TermsFile = data.file;
@ -20,6 +20,12 @@ function init(data)
Engine.GetGUIObjectByName("title").caption = data.title;
initURLButtons(data.termsURL, data.urlButtons);
initLanguageSelection();
const accepted = await new Promise(resolve => {
Engine.GetGUIObjectByName("cancelButton").onPress = resolve.bind(null, false);
Engine.GetGUIObjectByName("connectButton").onPress = resolve.bind(null, true);
});
return arangeReturnValue(accepted);
}
function initURLButtons(termsURL, urlButtons)
@ -83,10 +89,10 @@ function initLanguageSelection()
languageDropdown.selected = languageDropdown.list.length - 1;
}
function closeTerms(accepted)
function arangeReturnValue(accepted)
{
Engine.PopGuiPage({
return {
"page": g_TermsPage,
"accepted": accepted
});
};
}

View File

@ -29,14 +29,12 @@
<object size="170 0 330 100%" type="button" name="button[1]" style="ModernButtonRed" hidden="true"/>
<object size="100%-355 0 100% 100%">
<object type="button" style="ModernButtonRed" size="0 0 160 100%" hotkey="cancel">
<object name="cancelButton" type="button" style="ModernButtonRed" size="0 0 160 100%" hotkey="cancel">
<translatableAttribute id="caption">Decline</translatableAttribute>
<action on="Press">closeTerms(false);</action>
</object>
<object name="connectButton" type="button" style="ModernButtonRed" size="170 0 330 100%">
<translatableAttribute id="caption">Accept</translatableAttribute>
<action on="Press">closeTerms(true);</action>
</object>
</object>
</object>

View File

@ -15,15 +15,14 @@ class TimedConfirmation
* @param {String|undefined} data.buttonCaptions - The captions used for buttons (if not defined, defaults to 'OK')
* @param {String|undefined} data.font - The used font
*/
constructor(data)
constructor()
{
this.messageObject = Engine.GetGUIObjectByName("tmcText");
this.panel = Engine.GetGUIObjectByName("tmcMain");
this.panel.onTick = this.onTick.bind(this);
this.setup(data);
}
async setup(data)
setup(data)
{
Engine.GetGUIObjectByName("tmcTitleBar").caption = data.title;
@ -37,7 +36,6 @@ class TimedConfirmation
this.updateDisplayedTimer(data.timeout);
const cancelHotkey = Engine.GetGUIObjectByName("tmcCancelHotkey");
cancelHotkey.onPress = Engine.PopGuiPage;
const lRDiff = data.width / 2;
const uDDiff = data.height / 2;
@ -49,8 +47,7 @@ class TimedConfirmation
const closePromise =
setButtonCaptionsAndVisibitily(button, captions, cancelHotkey, "tmcButton");
distributeButtonsHorizontally(button, captions);
Engine.PopGuiPage(await closePromise);
return closePromise;
}
onTick()
@ -73,5 +70,5 @@ class TimedConfirmation
function init(data)
{
new TimedConfirmation(data);
return new TimedConfirmation().setup(data);
}

View File

@ -5,11 +5,11 @@
*/
class NewCampaignModal
{
constructor(campaignTemplate)
constructor(campaignTemplate, popGuiPage)
{
this.template = campaignTemplate;
Engine.GetGUIObjectByName('cancelButton').onPress = () => Engine.PopGuiPage();
Engine.GetGUIObjectByName('cancelButton').onPress = popGuiPage;
Engine.GetGUIObjectByName('startButton').onPress = () => this.createAndStartCampaign();
Engine.GetGUIObjectByName('runDescription').caption = translateWithContext("Campaign Template", this.template.Name);
Engine.GetGUIObjectByName('runDescription').onTextEdit = () => {
@ -38,5 +38,7 @@ var g_NewCampaignModal;
function init(campaign_template_data)
{
g_NewCampaignModal = new NewCampaignModal(campaign_template_data);
return new Promise(popGuiPage => {
g_NewCampaignModal = new NewCampaignModal(campaign_template_data, popGuiPage);
});
}

View File

@ -59,6 +59,8 @@ function init()
category => {
Engine.GetGUIObjectByName("creditsText").caption = g_PanelData[category].content;
});
return new Promise(popGuiPage => { Engine.GetGUIObjectByName("closeButton").onPress = popGuiPage; });
}
// Run through a "Content" list and parse elements for formatting and translation

View File

@ -28,11 +28,8 @@
</object>
<!-- Close dialog -->
<object type="button" style="ModernButtonRed" size="100%-200 100%-45 100%-17 100%-17" hotkey="cancel">
<object name="closeButton" type="button" style="ModernButtonRed" size="100%-200 100%-45 100%-17 100%-17" hotkey="cancel">
<translatableAttribute id="caption">Close</translatableAttribute>
<action on="Press">
Engine.PopGuiPage();
</action>
</object>
</object>
</objects>

View File

@ -24,7 +24,7 @@ var g_IsRejoining = false;
var g_PlayerAssignments; // used when rejoining
var g_UserRating;
function init(attribs)
async function init(attribs)
{
g_UserRating = attribs.rating;
@ -45,6 +45,8 @@ function init(attribs)
}
else if (startJoinFromLobby(attribs.name, attribs.hostJID, ""))
switchSetupPage("pageConnecting");
else if (cancelSetup())
return;
break;
}
case "host":
@ -68,6 +70,22 @@ function init(attribs)
error("Unrecognised multiplayer game type: " + attribs.multiplayerGameType);
break;
}
while (true)
{
await new Promise(resolve => {
Engine.GetGUIObjectByName("cancelButton").onPress = () => { resolve(); };
Engine.GetGUIObjectByName("multiplayerPages").onTick = () => { onTick() && resolve(); };
Engine.GetGUIObjectByName("continueButton").onPress = () => {
confirmSetup() && resolve();
};
Engine.GetGuiObjectByName("confirmPasswordButton").onPress = () => {
confirmPassword() && resolve();
};
});
if (cancelSetup())
return;
}
}
function cancelSetup()
@ -80,10 +98,7 @@ function cancelSetup()
// Keep the page open if an attempt to join/host by ip failed
if (!g_IsConnecting || (Engine.HasXmppClient() && g_GameType == "client"))
{
Engine.PopGuiPage();
return;
}
return true;
g_IsConnecting = false;
Engine.GetGUIObjectByName("hostFeedback").caption = "";
@ -94,17 +109,22 @@ function cancelSetup()
switchSetupPage("pageHost");
else
error("cancelSetup: Unrecognised multiplayer game type: " + g_GameType);
return false;
}
function confirmPassword()
{
if (Engine.GetGUIObjectByName("pagePassword").hidden)
return;
return false;
if (startJoinFromLobby(g_ServerName, g_ServerId, Engine.GetGUIObjectByName("clientPassword").caption))
{
switchSetupPage("pageConnecting");
return false;
}
return true;
}
function confirmSetup()
function confirmSetup(popGuiPage)
{
if (!Engine.GetGUIObjectByName("pageJoin").hidden)
{
@ -113,15 +133,20 @@ function confirmSetup()
let joinPort = Engine.GetGUIObjectByName("joinPort").caption;
if (startJoin(joinPlayerName, joinServer, getValidPort(joinPort)))
{
switchSetupPage("pageConnecting");
return false;
}
else if (!Engine.GetGUIObjectByName("pageHost").hidden)
return true
}
if (!Engine.GetGUIObjectByName("pageHost").hidden)
{
let hostServerName = Engine.GetGUIObjectByName("hostServerName").caption;
if (!hostServerName)
{
Engine.GetGUIObjectByName("hostFeedback").caption = translate("Please enter a valid server name.");
return;
return false;
}
let hostPort = Engine.GetGUIObjectByName("hostPort").caption;
@ -132,13 +157,17 @@ function confirmSetup()
"min": g_ValidPorts.min,
"max": g_ValidPorts.max
});
return;
return false;
}
let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption;
let hostPassword = Engine.GetGUIObjectByName("hostPassword").caption;
if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword))
if (startHost(popGuiPage, hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword))
{
switchSetupPage("pageConnecting");
return false;
}
return true;
}
}
@ -150,12 +179,12 @@ function startConnectionStatus(type)
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Connecting to server...");
}
function onTick()
function onTick(popGuiPage)
{
if (!g_IsConnecting)
return;
return false;
pollAndHandleNetworkClient();
return pollAndHandleNetworkClient();
}
function getConnectionFailReason(reason)
@ -202,9 +231,8 @@ function pollAndHandleNetworkClient()
switch (message.status)
{
case "failed":
cancelSetup();
reportConnectionFail(message.reason, false);
return;
return true;
default:
error("Unrecognised netstatus type: " + message.status);
@ -216,9 +244,8 @@ function pollAndHandleNetworkClient()
switch (message.status)
{
case "disconnected":
cancelSetup();
reportDisconnect(message.reason, false);
return;
return true;
default:
error("Unrecognised netstatus type: " + message.status);
@ -238,7 +265,7 @@ function pollAndHandleNetworkClient()
});
// Process further pending netmessages in the session page
return;
return false;
case "chat":
break;
@ -259,9 +286,8 @@ function pollAndHandleNetworkClient()
switch (message.status)
{
case "failed":
cancelSetup();
reportConnectionFail(message.reason, false);
return;
return true;
default:
error("Unrecognised netstatus type: " + message.status);
@ -281,18 +307,17 @@ function pollAndHandleNetworkClient()
{
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Game has already started, rejoining...");
g_IsRejoining = true;
return; // we'll process the game setup messages in the next tick
return false; // we'll process the game setup messages in the next tick
}
Engine.SwitchGuiPage("page_gamesetup.xml", {
"serverName": g_ServerName,
"hasPassword": g_ServerHasPassword
});
return; // don't process any more messages - leave them for the game GUI loop
return false; // don't process any more messages - leave them for the game GUI loop
case "disconnected":
cancelSetup();
reportDisconnect(message.reason, false);
return;
return true;
default:
error("Unrecognised netstatus type: " + message.status);
@ -357,7 +382,6 @@ function startHost(playername, servername, port, password)
if (Engine.HasXmppClient() &&
Engine.GetGameList().some(game => game.name == servername))
{
cancelSetup();
hostFeedback.caption = translate("Game name already in use.");
return false;
}
@ -370,7 +394,6 @@ function startHost(playername, servername, port, password)
}
catch (e)
{
cancelSetup();
messageBox(
400, 200,
sprintf(translate("Cannot host game: %(message)s."), { "message": e.message }),
@ -399,7 +422,6 @@ function startJoin(playername, ip, port)
}
catch (e)
{
cancelSetup();
messageBox(
400, 200,
sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }),
@ -429,7 +451,6 @@ function startJoinFromLobby(playername, hostJID, password)
{
if (!Engine.HasXmppClient())
{
cancelSetup();
messageBox(
400, 200,
sprintf("You cannot join a lobby game without logging in to the lobby."),
@ -444,7 +465,6 @@ function startJoinFromLobby(playername, hostJID, password)
}
catch (e)
{
cancelSetup();
messageBox(
400, 200,
sprintf(translate("Cannot join game: %(message)s."), { "message": e.message }),

View File

@ -10,10 +10,6 @@
<object name="multiplayerPages" type="image" style="ModernDialog" size="50%-230 50%-120 50%+230 50%+120">
<action on="Tick">
onTick();
</action>
<object style="ModernLabelText" type="text" size="50%-128 0%-16 50%+128 16">
<translatableAttribute id="caption">Multiplayer</translatableAttribute>
</object>
@ -134,12 +130,10 @@
<object name="continueButton" hotkey="confirm" type="button" size="50%+5 100%-45 100%-18 100%-17" style="ModernButtonRed">
<translatableAttribute id="caption">Continue</translatableAttribute>
<action on="Press">confirmSetup();</action>
</object>
<object type="button" style="ModernButtonRed" size="18 100%-45 50%-5 100%-17" hotkey="cancel">
<object name="cancelButton" type="button" style="ModernButtonRed" size="18 100%-45 50%-5 100%-17" hotkey="cancel">
<translatableAttribute id="caption">Cancel</translatableAttribute>
<action on="Press">cancelSetup();</action>
</object>
<object name="pageConnecting" hidden="true">
@ -160,7 +154,6 @@
</object>
<object name="confirmPasswordButton" hotkey="confirm" type="button" size="50%+5 100%-45 100%-18 100%-17" style="ModernButtonRed">
<translatableAttribute id="caption">Confirm</translatableAttribute>
<action on="Press">confirmPassword();</action>
</object>
</object>
</object>

View File

@ -1,6 +1,6 @@
class HotkeysPage
{
constructor(metadata)
constructor(metadata, popGuiPage)
{
this.metadata = metadata;
@ -23,7 +23,7 @@ class HotkeysPage
this.saveButton = Engine.GetGUIObjectByName("hotkeySave");
this.saveButton.enabled = false;
Engine.GetGUIObjectByName("hotkeyClose").onPress = () => Engine.PopGuiPage();
Engine.GetGUIObjectByName("hotkeyClose").onPress = popGuiPage;
Engine.GetGUIObjectByName("hotkeyReset").onPress = () => this.resetUserHotkeys();
this.saveButton.onPress = () => {
this.saveUserHotkeys();
@ -190,7 +190,9 @@ class HotkeysPage
function init()
{
let hotkeyPage = new HotkeysPage(new HotkeyMetadata());
return new Promise(popGuiPage => {
let hotkeyPage = new HotkeysPage(new HotkeyMetadata(), popGuiPage);
});
}
HotkeysPage.prototype.UnavailableTooltipString = markForTranslation("No tooltip available.");

View File

@ -3,8 +3,9 @@
*/
class SavegameLoader
{
constructor()
constructor(popGuiPage)
{
this.popGuiPage = popGuiPage;
this.confirmButton = Engine.GetGUIObjectByName("confirmButton");
this.confirmButton.caption = translate("Load");
this.confirmButton.enabled = false;
@ -27,7 +28,7 @@ class SavegameLoader
if (sameEngineVersion && sameMods)
{
Engine.PopGuiPage(gameId);
this.popGuiPage(gameId);
return;
}
@ -60,6 +61,6 @@ class SavegameLoader
translate("Warning"),
[translate("No"), translate("Yes")]);
if (buttonIndex === 1)
Engine.PopGuiPage(gameId);
this.popGuiPage(gameId);
}
}

View File

@ -4,7 +4,7 @@
*/
class SavegamePage
{
constructor(data)
constructor(data, popGuiPage)
{
this.savegameList = new SavegameList(data && data.campaignRun || null);
@ -18,7 +18,7 @@ class SavegamePage
const savePage = !!data?.savedGameData;
if (savePage)
{
this.savegameWriter = new SavegameWriter(data.savedGameData);
this.savegameWriter = new SavegameWriter(popGuiPage, data.savedGameData);
this.savegameList.registerSelectionChangeHandler(this.savegameWriter);
let size = this.savegameList.gameSelection.size;
size.bottom -= 24;
@ -26,13 +26,13 @@ class SavegamePage
}
else
{
this.savegameLoader = new SavegameLoader();
this.savegameLoader = new SavegameLoader(popGuiPage);
this.savegameList.registerSelectionChangeHandler(this.savegameLoader);
this.savegameList.selectFirst();
}
Engine.GetGUIObjectByName("title").caption = savePage ? translate("Save Game") : translate("Load Game");
Engine.GetGUIObjectByName("cancel").onPress = () => { Engine.PopGuiPage(); };
Engine.GetGUIObjectByName("cancel").onPress = popGuiPage;
}
}
@ -40,5 +40,5 @@ var g_SavegamePage;
function init(data)
{
g_SavegamePage = new SavegamePage(data);
return new Promise(popGuiPage => { g_SavegamePage = new SavegamePage(data, popGuiPage); });
}

View File

@ -4,8 +4,9 @@
*/
class SavegameWriter
{
constructor(savedGameData)
constructor(popGuiPage, savedGameData)
{
this.popGuiPage = popGuiPage;
this.savedGameData = savedGameData;
let saveNew = () => {
@ -58,6 +59,6 @@ class SavegameWriter
Engine[gameID ? "SaveGame" : "SaveGamePrefix"](name, desc, this.savedGameData);
Engine.PopGuiPage();
this.popGuiPage();
}
}

View File

@ -9,7 +9,7 @@ class LobbyHandler
this.profilePage = new ProfilePage(this.xmppMessages);
this.leaderboardPage = new LeaderboardPage(this.xmppMessages);
this.lobbyPage = new LobbyPage(dialog, this.xmppMessages, this.leaderboardPage, this.profilePage);
this.lobbyPage = new LobbyPage(popGuiPage, dialog, this.xmppMessages, this.leaderboardPage, this.profilePage);
this.xmppMessages.processHistoricMessages();

View File

@ -3,8 +3,9 @@
*/
class QuitButton
{
constructor(dialog, leaderboardPage, profilePage)
constructor(popGuiPage, dialog, leaderboardPage, profilePage)
{
this.popGuiPage = popGuiPage;
let closeDialog = this.closeDialog.bind(this);
let returnToMainMenu = this.returnToMainMenu.bind(this);
let onPress = dialog ? closeDialog : returnToMainMenu;
@ -29,7 +30,7 @@ class QuitButton
closeDialog()
{
Engine.LobbySetPlayerPresence("playing");
Engine.PopGuiPage();
this.popGuiPage();
}
returnToMainMenu()

View File

@ -4,7 +4,7 @@
*/
class LobbyPage
{
constructor(dialog, xmppMessages, leaderboardPage, profilePage)
constructor(popGuiPage, dialog, xmppMessages, leaderboardPage, profilePage)
{
Engine.ProfileStart("Create LobbyPage");
let mapCache = new MapCache();
@ -19,7 +19,7 @@ class LobbyPage
"joinButton": new JoinButton(dialog, gameList),
"leaderboardButton": new LeaderboardButton(xmppMessages, leaderboardPage),
"profileButton": new ProfileButton(xmppMessages, profilePage),
"quitButton": new QuitButton(dialog, leaderboardPage, profilePage)
"quitButton": new QuitButton(popGuiPage, dialog, leaderboardPage, profilePage)
},
"panels": {
"chatPanel": new ChatPanel(xmppMessages),

View File

@ -34,7 +34,9 @@ var g_LobbyHandler;
function init(attribs)
{
if (g_Settings)
g_LobbyHandler = new LobbyHandler(attribs && attribs.dialog);
else
return new Promise(popGuiPage => {
g_LobbyHandler = new LobbyHandler(popGuiPage, attribs && attribs.dialog)
};
error("Could not load settings");
}

View File

@ -21,11 +21,8 @@ function init()
localeText.caption = "long";
else
localeText.caption = currentLocale;
}
function cancelSetup()
{
Engine.PopGuiPage();
return new Promise(popGuiPage => { Engine.GetGUIObjectByName("cancelButton").onPress = popGuiPage; });
}
function applySelectedLocale()

View File

@ -32,9 +32,8 @@
</object>
<object name="localeText" type="text" size="40%+10 80 100% 105" textcolor="white" />
<object type="button" size="0 100%-60 33% 100%-32" style="ModernButtonRed" hotkey="cancel">
<object name="cancelButton" type="button" size="0 100%-60 33% 100%-32" style="ModernButtonRed" hotkey="cancel">
<translatableAttribute id="caption">Cancel</translatableAttribute>
<action on="Press">cancelSetup();</action>
</object>
<object type="button" size="33%+5 100%-60 66% 100%-32" style="ModernButtonRed">

View File

@ -46,6 +46,13 @@ function init(initData)
// fill the script
scriptInput.caption = Engine.GetLocaleScript(initData.locale);
return new Promise(popGuiPage => {
Engine.GetGUIObjectByName("cancelButton").onPress = popGuiPage;
Engine.GetGUIObjectByName("acceptButton").onPress = () => {
popGuiPage(applySelectedLocale());
};
});
}
// TODO: an onChanged event for input boxes would be useful and would allow us to avoid a tick event here.
@ -54,11 +61,6 @@ function onTick()
updateResultingLocale();
}
function cancelSetup()
{
Engine.PopGuiPage();
}
function updateResultingLocale()
{
var languageList = Engine.GetGUIObjectByName("languageList");
@ -113,6 +115,5 @@ function autoDetectLocale()
function applySelectedLocale()
{
var resultingLocaleText = Engine.GetGUIObjectByName("resultingLocale");
Engine.PopGuiPage(resultingLocaleText.caption);
return Engine.GetGUIObjectByName("resultingLocale").caption;
}

View File

@ -61,9 +61,8 @@
</object>
<object name="dictionaryFile" type="text" size="50%+10 260 100% 345" style="ModernText" />
<object type="button" size="0 100%-60 33% 100%-25" style="ModernButtonRed" hotkey="cancel">
<object name="cancelButton" type="button" size="0 100%-60 33% 100%-25" style="ModernButtonRed" hotkey="cancel">
<translatableAttribute id="caption">Cancel</translatableAttribute>
<action on="Press">cancelSetup();</action>
</object>
<object type="button" size="33%+5 100%-60 66% 100%-25" style="ModernButtonRed">
@ -73,7 +72,6 @@
<object name="acceptButton" type="button" style="ModernButtonRed" size="66%+5 100%-60 100% 100%-25">
<translatableAttribute id="caption">Accept</translatableAttribute>
<action on="Press">applySelectedLocale();</action>
</object>
</object>
</object>

View File

@ -7,4 +7,6 @@ function init()
// Replace anything starting with 'hotkey.' with its hotkey.
mainText.caption = text.replace(/hotkey\.([a-z0-9_\.]+)/g, (_, k) => formatHotkeyCombinations(hotkeys[k]));
return new Promise(popGuiPage => { Engine.GetGUIObjectByName("closeButton").onPress = popGuiPage; });
}

View File

@ -17,9 +17,8 @@
<object name="mainText" type="text" style="ModernTextPanel" />
</object>
<object type="button" style="ModernButtonRed" tooltip_style="snToolTip" size="100%-408 100%-52 100%-218 100%-24" hotkey="cancel">
<object name="closeButton" 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">Engine.PopGuiPage();</action>
</object>
<object name="url" type="button" style="ModernButtonRed" size="100%-214 100%-52 100%-24 100%-24">

View File

@ -8,7 +8,7 @@ function init()
let cache = new MapCache();
let filters = new MapFilters(cache);
let browser = new MapBrowser(cache, filters);
browser.registerClosePageHandler(() => Engine.PopGuiPage());
browser.openPage(false);
browser.controls.MapFiltering.select("default", "skirmish");
return new Promise(popGuiPage => { browser.registerClosePageHandler(popGuiPage); });
}

View File

@ -225,6 +225,12 @@ function init(data, hotloadData)
g_TabButtonDist,
selectPanel,
displayOptions);
return new Promise(popGuiPage => {
Engine.GetGUIObjectByName("closeButton").onPress = () => {
closePage() && popGuiPage(g_ChangedKeys);
};
});
}
function getHotloadData()
@ -448,8 +454,8 @@ async function closePage()
translate("Warning"),
[translate("No"), translate("Yes")]);
if (buttonIndex === 0)
return;
return false;
}
Engine.PopGuiPage(g_ChangedKeys);
return true;
}

View File

@ -58,10 +58,9 @@
<action on="Press">saveChanges();</action>
</object>
<object type="button" style="ModernButtonRed" size="50%+186 100%-44 50%+336 100%-16" hotkey="cancel">
<object name="closeButton" type="button" style="ModernButtonRed" size="50%+186 100%-44 50%+336 100%-16" hotkey="cancel">
<translatableAttribute id="caption">Close</translatableAttribute>
<translatableAttribute id="tooltip">Unsaved changes affect this session only</translatableAttribute>
<action on="Press">closePage();</action>
</object>
</object>

View File

@ -37,9 +37,9 @@ function setFeedback(feedbackText)
Engine.GetGUIObjectByName("continue").enabled = !feedbackText;
}
function cancelButton()
async function cancelButton()
{
await new Promise(resolve => { Engine.GetGUIPageByName("cancel").onPress = resolve; });
if (Engine.HasXmppClient())
Engine.StopXmppClient();
Engine.PopGuiPage();
}

View File

@ -8,7 +8,6 @@
<object name="cancel" type="button" size="0 0 50%-5 100%" style="ModernButtonRed" hotkey="cancel">
<translatableAttribute id="caption">Cancel</translatableAttribute>
<action on="Press">cancelButton();</action>
</object>
<object name="continue" hotkey="confirm" type="button" size="50%+5 0 100% 100%" style="ModernButtonRed" enabled="false">

View File

@ -2,6 +2,8 @@ function init()
{
if (Engine.ConfigDB_GetValue("user", "lobby.login"))
loginButton();
return new Promise(popGuiPage => { Engine.GetGUIObjectByName("cancel").onPress = popGuiPage; });
}
function loginButton()
@ -13,8 +15,3 @@ function registerButton()
{
Engine.PushGuiPage("page_prelobby_register.xml");
}
function cancelButton()
{
Engine.PopGuiPage();
}

View File

@ -28,7 +28,6 @@
<object name="cancel" type="button" size="18 100%-45 50%-5 100%-17" style="ModernButtonRed" hotkey="cancel">
<translatableAttribute id="caption">Cancel</translatableAttribute>
<action on="Press">cancelButton();</action>
</object>
</object>

View File

@ -13,6 +13,8 @@ function init()
initRememberPassword();
updateFeedback();
return cancelButton();
}
function updateFeedback()

View File

@ -1,7 +1,5 @@
function init()
{
g_LobbyMessages.registered = onRegistered;
Engine.GetGUIObjectByName("continue").caption = translate("Register");
initLobbyTerms();
@ -9,6 +7,8 @@ function init()
initRememberPassword();
updateFeedback();
return Promise.race([ onRegistered(), cancelButton() ]);
}
function updateFeedback()
@ -32,14 +32,14 @@ function continueButton()
Engine.ConnectXmppClient();
}
function onRegistered()
async function onRegistered()
{
await new Promise(resolve => { g_LobbyMessages.registered = resolve; });
saveCredentials();
setFeedback(translate("Registered"));
Engine.StopXmppClient();
Engine.PopGuiPage();
Engine.PushGuiPage("page_prelobby_login.xml");
}

View File

@ -1,8 +1,8 @@
class CatafalquePage extends ReferencePage
{
constructor(data)
constructor(popGuiPage)
{
super();
super(popGuiPage);
this.Canvas = Engine.GetGUIObjectByName("canvas");
this.Emblems = [];
@ -31,7 +31,7 @@ class CatafalquePage extends ReferencePage
closePage()
{
Engine.PopGuiPage({ "page": "page_catafalque.xml" });
this.popGuiPage({ "page": "page_catafalque.xml" });
}
}

View File

@ -1,4 +1,4 @@
function init(data = {})
{
g_Page = new CatafalquePage(data);
return new Promise(popGuiPage => { g_Page = new CatafalquePage(popGuiPage); });
}

View File

@ -1,8 +1,8 @@
class CivInfoPage extends ReferencePage
{
constructor(data)
constructor(popGuiPage)
{
super();
super(popGuiPage);
this.civSelection = new CivSelectDropdown(this.civData);
this.civSelection.registerHandler(this.selectCiv.bind(this));
@ -17,7 +17,7 @@ class CivInfoPage extends ReferencePage
switchToStructreePage()
{
Engine.PopGuiPage({
this.popGuiPage({
"nextPage": "page_structree.xml",
"args": {
"civ": this.activeCiv
@ -27,7 +27,7 @@ class CivInfoPage extends ReferencePage
closePage()
{
Engine.PopGuiPage({
this.popGuiPage({
"page": "page_civinfo.xml",
"args": {
"civ": this.activeCiv

View File

@ -3,10 +3,12 @@
*/
function init(data = {})
{
g_Page = new CivInfoPage(data);
const promise = new Promise(popGuiPage => { g_Page = new CivInfoPage(data, popGuiPage); });
if (data.civ)
g_Page.civSelection.selectCiv(data.civ);
else
g_Page.civSelection.selectFirstCiv();
return promise;
}

View File

@ -12,7 +12,7 @@ class CivInfoButton
onPress()
{
Engine.PopGuiPage({
this.parentPage.popGuiPage({
"nextPage": "page_civinfo.xml",
"args": {
"civ": this.parentPage.activeCiv

View File

@ -12,7 +12,7 @@ class StructreeButton
onPress()
{
Engine.PopGuiPage({
this.parentPage.popGuiPage({
"nextPage": "page_structree.xml",
"args": {
"civ": this.parentPage.activeCiv

View File

@ -3,8 +3,9 @@
*/
class ReferencePage
{
constructor()
constructor(popGuiPage)
{
this.popGuiPage = popGuiPage;
this.civData = loadCivData(true, false);
this.TemplateLoader = new TemplateLoader();

View File

@ -5,9 +5,9 @@
*/
class StructreePage extends ReferencePage
{
constructor(data)
constructor(popGuiPage)
{
super();
super(popGuiPage);
this.structureBoxes = [];
this.trainerBoxes = [];
@ -39,7 +39,7 @@ class StructreePage extends ReferencePage
closePage()
{
Engine.PopGuiPage({
this.popGuiPage({
"page": "page_structree.xml",
"args": {
"civ": this.activeCiv

View File

@ -5,10 +5,12 @@
*/
function init(data = {})
{
g_Page = new StructreePage(data);
const promise = new Promise(popGuiPage => {g_Page = new StructreePage(popGuiPage)};
if (data.civ)
g_Page.civSelection.selectCiv(data.civ);
else
g_Page.civSelection.selectFirstCiv();
return promise;
}

View File

@ -3,9 +3,9 @@
*/
class ViewerPage extends ReferencePage
{
constructor(data)
constructor(popGuiPage)
{
super();
super(popGuiPage);
this.currentTemplate = undefined;
@ -140,7 +140,7 @@ class ViewerPage extends ReferencePage
closePage()
{
Engine.PopGuiPage({ "civ": this.activeCiv, "page": "page_viewer.xml" });
this.popGuiPage({ "civ": this.activeCiv, "page": "page_viewer.xml" });
}
}

View File

@ -14,6 +14,7 @@ g_TooltipTextFormats.nameSecondary.font = "sans-bold-16";
*/
function init(data)
{
g_Page = new ViewerPage();
const promise = new Promise(popGuiPage => { g_Page = new ViewerPage(popGuiPage); });
g_Page.selectTemplate(data);
return promise;
}

View File

@ -1,9 +1,9 @@
class CampaignSession
{
constructor(data)
constructor(data, popGuiPage)
{
this.run = new CampaignRun(data.run).load();
registerPlayersFinishedHandler(this.onFinish.bind(this));
registerPlayersFinishedHandler(this.onFinish.bind(this, popGuiPage));
this.endGameData = {
"won": false,
"initData": data,
@ -11,7 +11,7 @@ class CampaignSession
};
}
onFinish(players, won)
onFinish(popGuiPage, players, won)
{
let playerID = Engine.GetPlayerID();
if (players.indexOf(playerID) === -1)
@ -24,7 +24,7 @@ class CampaignSession
// Run the endgame script.
Engine.PushGuiPage(this.getEndGame(), this.endGameData);
Engine.PopGuiPage();
popGuiPage();
}
getMenu()

View File

@ -270,8 +270,10 @@ function init(initData, hotloadData)
restoreSavedGameData(initData.savedGUIData);
}
const promise = new Promise(popGuiPage => {
if (g_InitAttributes.campaignData)
g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData);
g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData, popGuiPage);
});
let mapCache = new MapCache();
g_Cheats = new Cheats();
@ -332,6 +334,8 @@ function init(initData, hotloadData)
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
return promise;
}
function registerPlayersInitHandler(handler)

View File

@ -1,15 +1,15 @@
var g_SplashScreenFile = "gui/splashscreen/splashscreen.txt";
function init(data)
async function init(data)
{
Engine.GetGUIObjectByName("mainText").caption = Engine.TranslateLines(Engine.ReadFile(g_SplashScreenFile));
Engine.GetGUIObjectByName("displaySplashScreen").checked = Engine.ConfigDB_GetValue("user", "gui.splashscreen.enable") === "true";
}
function closePage()
{
await new Promise(resolve => {
Engine.GetGUIObjectByName("btnOK").onPress = resolve;
});
Engine.ConfigDB_CreateValue("user", "gui.splashscreen.enable", String(Engine.GetGUIObjectByName("displaySplashScreen").checked));
Engine.ConfigDB_CreateValue("user", "gui.splashscreen.version", Engine.GetFileMTime(g_SplashScreenFile));
Engine.ConfigDB_SaveChanges("user");
Engine.PopGuiPage();
}

View File

@ -23,7 +23,6 @@
<object name="btnOK" type="button" style="ModernButtonRed" size="18 100%-45 50%-5 100%-17" hotkey="cancel">
<translatableAttribute id="caption">OK</translatableAttribute>
<action on="Press">closePage();</action>
</object>
<object type="button" style="ModernButtonRed" size="50%+5 100%-45 100%-18 100%-17">

View File

@ -135,10 +135,25 @@ var g_SelectedChart = {
"type": [0, 0]
};
function init(data)
async function init(data)
{
initSummaryData(data);
initGUISummary();
while (true)
{
const branchless await new Promise(popGuiPage => {
Engine.GetGUIObjectByName("summaryHotkey").onPress = resolve(true);
Engine.GetGUIObjectByName("cancelHotkey").onPress = resolve(false);
});
if (branchless || g_GameData.gui.isInGame)
{
const result = continueButton();
if (result !== undefined)
return result ?? undefined;
}
}
}
function initSummaryData(data)
@ -461,11 +476,11 @@ function continueButton()
"teamCharts": Engine.GetGUIObjectByName("toggleTeamBox").checked
};
if (g_GameData.gui.isInGame)
Engine.PopGuiPage({
return {
"summarySelection": summarySelection
});
else if (g_GameData.gui.dialog)
Engine.PopGuiPage();
};
if (g_GameData.gui.dialog)
return null;
else if (Engine.HasXmppClient())
Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
else if (g_GameData.gui.isReplay)
@ -477,6 +492,8 @@ function continueButton()
Engine.SwitchGuiPage(g_GameData.nextPage, g_GameData.campaignData);
else
Engine.SwitchGuiPage("page_pregame.xml");
return undefined;
}
function startReplay()

View File

@ -5,16 +5,8 @@
<script directory="gui/common/"/>
<script directory="gui/summary/"/>
<object hotkey="summary">
<action on="Press">continueButton();</action>
</object>
<object hotkey="cancel">
<action on="Press">
if (g_GameData.gui.isInGame)
continueButton();
</action>
</object>
<object name="summaryHotkey" hotkey="summary"/>
<object name="cancelHotkey" hotkey="cancel"/>
<object type="image" sprite="ModernFade" name="fadeImage"/>

View File

@ -137,26 +137,8 @@ JS::Value CGUIManager::PushPage(const CStrW& pageName, Script::StructuredClone i
return promise;
}
void CGUIManager::PopPage(Script::StructuredClone args)
{
if (m_PageStack.size() < 2)
{
debug_warn(L"Tried to pop GUI page when there's < 2 in the stack");
return;
}
// Make sure we unfocus anything on the current page.
m_PageStack.back().gui->SendFocusMessage(GUIM_LOST_FOCUS);
m_PageStack.pop_back();
m_PageStack.back().ResolvePromise(args);
// We return to a page where some object might have been focused.
m_PageStack.back().gui->SendFocusMessage(GUIM_GOT_FOCUS);
}
CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const Script::StructuredClone initData)
: m_Name(pageName), initData(initData), inputs(), gui(), callbackFunction()
: m_Name(pageName), initData(initData)
{
}
@ -178,6 +160,9 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
g_VideoMode.ResetCursor();
inputs.clear();
gui.reset(new CGUI(scriptContext));
const ScriptRequest rq{gui->GetScriptInterface()};
sendingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx,
JS::NewPromiseObject(rq.cx, nullptr));
gui->AddObjectTypes();
@ -232,9 +217,6 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
gui->LoadedXmlFiles();
std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue initDataVal(rq.cx);
JS::RootedValue hotloadDataVal(rq.cx);
JS::RootedValue global(rq.cx, rq.globalValue());
@ -245,40 +227,68 @@ void CGUIManager::SGUIPage::LoadPage(ScriptContext& scriptContext)
if (hotloadData)
Script::ReadStructuredClone(rq, hotloadData, &hotloadDataVal);
if (Script::HasProperty(rq, global, "init") &&
!ScriptFunction::CallVoid(rq, global, "init", initDataVal, hotloadDataVal))
if (!Script::HasProperty(rq, global, "init"))
return;
JS::RootedValue returnValue{rq.cx};
if (!ScriptFunction::Call(rq, global, "init", &returnValue, initDataVal, hotloadDataVal))
{
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(m_Name));
return;
}
if (!returnValue.isObject())
return;
JS::RootedObject returnObject{rq.cx, &returnValue.toObject()};
if (!JS::IsPromiseObject(returnObject))
return;
sendingPromise = std::make_shared<JS::PersistentRootedObject>(rq.cx, returnObject);
}
JS::Value CGUIManager::SGUIPage::ReplacePromise(ScriptInterface& scriptInterface)
{
JSContext* generalContext{scriptInterface.GetGeneralJSContext()};
callbackFunction = std::make_shared<JS::PersistentRootedObject>(generalContext,
receivingPromise = std::make_shared<JS::PersistentRootedObject>(generalContext,
JS::NewPromiseObject(generalContext, nullptr));
return JS::ObjectValue(**callbackFunction);
return JS::ObjectValue(**receivingPromise);
}
void CGUIManager::SGUIPage::ResolvePromise(Script::StructuredClone args)
CGUIManager::SGUIPage::CloseResult CGUIManager::SGUIPage::Close()
{
if (!callbackFunction)
return;
// Make sure we unfocus anything on the current page.
gui->SendFocusMessage(GUIM_LOST_FOCUS);
const ScriptRequest rq{gui->GetScriptInterface()};
JS::RootedValue arg{rq.cx, JS::GetPromiseResult(*sendingPromise)};
return {Script::WriteStructuredClone(rq, arg),
JS::GetPromiseState(*sendingPromise) == JS::PromiseState::Rejected};
}
void CGUIManager::SGUIPage::Refocus(const CloseResult& result)
{
ENSURE(receivingPromise);
std::shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedObject globalObj(rq.cx, rq.glob);
JS::RootedObject funcVal(rq.cx, *callbackFunction);
JS::RootedObject recv(rq.cx, *receivingPromise);
// Delete the callback function, so that it is not called again
callbackFunction.reset();
receivingPromise.reset();
JS::RootedValue argVal(rq.cx);
if (args)
Script::ReadStructuredClone(rq, args, &argVal);
Script::ReadStructuredClone(rq, result.arg, &argVal);
// This only resolves the promise, it doesn't call the continuation.
JS::ResolvePromise(rq.cx, funcVal, argVal);
(result.rejected ? JS::RejectPromise : JS::ResolvePromise)(rq.cx, recv, argVal);
// We return to a page where some object might have been focused.
gui->SendFocusMessage(GUIM_GOT_FOCUS);
}
Status CGUIManager::ReloadChangedFile(const VfsPath& path)
@ -368,6 +378,19 @@ void CGUIManager::TickObjects()
{
PROFILE3("gui tick");
while (!m_PageStack.empty() &&
JS::GetPromiseState(*m_PageStack.back().sendingPromise) != JS::PromiseState::Pending)
{
const size_t stackSize{m_PageStack.size()};
const SGUIPage::CloseResult result{m_PageStack.back().Close()};
ENSURE(m_PageStack.size() == stackSize);
m_PageStack.pop_back();
if (!m_PageStack.empty())
m_PageStack.back().Refocus(result);
g_ScriptContext->RunJobs();
}
// We share the script context with everything else that runs in the same thread.
// This call makes sure we trigger GC regularly even if the simulation is not running.
m_ScriptContext.MaybeIncrementalGC(1.0f);

View File

@ -72,12 +72,6 @@ public:
*/
JS::Value PushPage(const CStrW& pageName, Script::StructuredClone initData);
/**
* 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(Script::StructuredClone args);
/**
* Called when a file has been modified, to hotload changes.
*/
@ -151,10 +145,17 @@ private:
*/
JS::Value ReplacePromise(ScriptInterface& scriptInterface);
struct CloseResult
{
Script::StructuredClone arg;
bool rejected;
};
CloseResult Close();
/**
* Execute the stored callback function with the given arguments.
*/
void ResolvePromise(Script::StructuredClone args);
void Refocus(const CloseResult& result);
std::wstring m_Name;
std::unordered_set<VfsPath> inputs; // for hotloading
@ -162,10 +163,16 @@ private:
std::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.
* When this promise is setteled this page wants to be closed. It
* fullfils with the page completion value.
*/
std::shared_ptr<JS::PersistentRootedObject> callbackFunction;
std::shared_ptr<JS::PersistentRootedObject> sendingPromise;
/**
* The parrent page waits on this promise. It also gets the
* completion value through this promise.
*/
std::shared_ptr<JS::PersistentRootedObject> receivingPromise;
};
std::shared_ptr<CGUI> top() const;

View File

@ -43,17 +43,6 @@ void SwitchGuiPage(const ScriptInterface& scriptInterface, const std::wstring& n
g_GUI->SwitchPage(name, &scriptInterface, initData);
}
void PopGuiPage(const ScriptRequest& rq, JS::HandleValue args)
{
if (g_GUI->GetPageCount() < 2)
{
ScriptException::Raise(rq, "Can't pop GUI pages when less than two pages are opened!");
return;
}
g_GUI->PopPage(Script::WriteStructuredClone(rq, args));
}
void SetCursor(const std::wstring& name)
{
g_VideoMode.SetCursor(name);
@ -79,7 +68,6 @@ void RegisterScriptFunctions(const ScriptRequest& rq)
{
ScriptFunction::Register<&PushGuiPage>(rq, "PushGuiPage");
ScriptFunction::Register<&SwitchGuiPage>(rq, "SwitchGuiPage");
ScriptFunction::Register<&PopGuiPage>(rq, "PopGuiPage");
ScriptFunction::Register<&SetCursor>(rq, "SetCursor");
ScriptFunction::Register<&ResetCursor>(rq, "ResetCursor");
ScriptFunction::Register<&TemplateExists>(rq, "TemplateExists");

View File

@ -26,12 +26,14 @@
#include "ps/GameSetup/GameSetup.h"
#include "ps/Hotkey.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptRequest.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/StructuredClone.h"
#include "scriptinterface/Object.h"
#include "js/Promise.h"
#include <memory>
#include <optional>
@ -202,35 +204,71 @@ public:
UnloadHotkeys();
}
static void CloseTopmostPage()
{
ScriptRequest rq{g_GUI->GetActiveGUI()->GetScriptInterface()};
JS::RootedValue global{rq.cx, rq.globalValue()};
TS_ASSERT(ScriptFunction::CallVoid(rq, global, "closePage"));
// Check if some promise are setteled and flush the promise-jobQueue.
g_GUI->TickObjects();
}
void test_PageRegainedFocusEvent()
{
// Load up a test page.
ScriptRequest rq{g_GUI->GetScriptInterface()};
JS::RootedValue val(rq.cx);
Script::CreateObject(rq, &val);
const Script::StructuredClone undefined{
Script::WriteStructuredClone(rq, JS::UndefinedHandleValue)};
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 0);
Script::StructuredClone data = Script::WriteStructuredClone(rq, JS::NullHandleValue);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data);
const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface());
ScriptRequest prq(pageScriptInterface);
JS::RootedValue global(prq.cx, prq.globalValue());
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
g_GUI->PopPage(data);
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", undefined);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
// This page instantly pushes an empty page with a callback that pops another page again.
g_GUI->PushPage(L"regainFocus/page_pushWithPopOnInit.xml", data);
g_GUI->PushPage(L"regainFocus/page_pushWithPopOnInit.xml", undefined);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 3);
// Pop the empty page
g_GUI->PopPage(data);
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 2);
scriptInterface->GetContext().RunJobs();
// Pop the empty page and execute the continuation.
CloseTopmostPage();
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 1);
CloseTopmostPage();
TS_ASSERT_EQUALS(g_GUI->GetPageCount(), 0);
}
void test_ResolveReject()
{
TestLogger logger;
constexpr std::array<std::tuple<bool, JS::PromiseState>, 2> testSteps{{
{false, JS::PromiseState::Fulfilled},
{true, JS::PromiseState::Rejected}}};
const ScriptRequest rq{g_GUI->GetScriptInterface()};
const Script::StructuredClone undefined{
Script::WriteStructuredClone(rq, JS::UndefinedHandleValue)};
g_GUI->PushPage(L"regainFocus/page_emptyPage.xml", undefined);
for (const auto& [reject, result] : testSteps)
{
const JS::RootedValue value{rq.cx, JS::BooleanValue(reject)};
const Script::StructuredClone clonedValue{Script::WriteStructuredClone(rq, value)};
const JS::RootedValue promise{rq.cx,
g_GUI->PushPage(L"resolveReject/page_resolveReject.xml", clonedValue)};
// Check if some promise are setteled and flush the promise-jobQueue.
g_GUI->TickObjects();
const JS::RootedObject promiseObject{rq.cx, &promise.toObject()};
TS_ASSERT_EQUALS(JS::GetPromiseState(promiseObject), result);
}
}
void test_Sequential()
{
const ScriptRequest rq{g_GUI->GetScriptInterface()};
const Script::StructuredClone undefined{
Script::WriteStructuredClone(rq, JS::UndefinedHandleValue)};
g_GUI->PushPage(L"sequential/page_sequential.xml", undefined);
}
};