Changeset View
Standalone View
source/gui/GUIManager.cpp
Show First 20 Lines • Show All 77 Lines • ▼ Show 20 Lines | |||||
{ | { | ||||
return !m_PageStack.empty(); | return !m_PageStack.empty(); | ||||
} | } | ||||
void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, JS::HandleValue initData) | void CGUIManager::SwitchPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, JS::HandleValue initData) | ||||
{ | { | ||||
// The page stack is cleared (including the script context where initData came from), | // The page stack is cleared (including the script context where initData came from), | ||||
// therefore we have to clone initData. | // therefore we have to clone initData. | ||||
shared_ptr<ScriptInterface::StructuredClone> initDataClone; | shared_ptr<ScriptInterface::StructuredClone> initDataClone; | ||||
if (!initData.isUndefined()) | if (!initData.isUndefined()) | ||||
{ | |||||
initDataClone = srcScriptInterface->WriteStructuredClone(initData); | initDataClone = srcScriptInterface->WriteStructuredClone(initData); | ||||
} | |||||
m_PageStack.clear(); | m_PageStack.clear(); | ||||
PushPage(pageName, initDataClone); | |||||
PushPage(pageName, nullptr, initDataClone, JS::UndefinedHandleValue); | |||||
} | } | ||||
void CGUIManager::PushPage(const CStrW& pageName, shared_ptr<ScriptInterface::StructuredClone> initData) | void CGUIManager::PushPage(const CStrW& pageName, ScriptInterface* srcScriptInterface, shared_ptr<ScriptInterface::StructuredClone> initData, JS::HandleValue callbackFunction) | ||||
{ | |||||
if (!m_PageStack.empty() && callbackFunction.isObject()) | |||||
{ | { | ||||
vladislavbelov: Why different runtimes? | |||||
Not Done Inline ActionsCan't this be done at GUIPage init instead? It looks a bit odd right now (and explains vlad's comment) wraitii: Can't this be done at GUIPage init instead? It looks a bit odd right now (and explains vlad's… | |||||
Done Inline Actions
The feature in this patch is that PushPage optionally receives a JS function that will be executed when closing the page. m_PageStack.back().callbackFunction is the unset callback function of the new GUI page to be pushed.
Yes, it can be moved, but including LoadPage in the SGUIPage constructor (i.e. calling LoadPage prior to push_back) reveals a bug in the codebase - some functions called in the CGUI` page constructor just end up fetching data from the topmost page instead of the constructed page itself (which breaks if the page isn't on the stack yet). A bit dirty, I will clean this ahead, but upload the version with everything combined as a backup to this diff. elexis: > Why different runtimes?
The feature in this patch is that `PushPage` optionally receives a… | |||||
m_PageStack.push_back(SGUIPage()); | JSContext* cxSrc = srcScriptInterface->GetContext(); | ||||
m_PageStack.back().name = pageName; | JSAutoRequest rqSrc(cxSrc); | ||||
m_PageStack.back().initData = initData; | JS::RootedObject callbackObj(cxSrc, &callbackFunction.toObject()); | ||||
elexisAuthorUnsubmitted Done Inline ActionsOne doesn't have to Root the object to test if it is a function. This means we only need the ScriptRuntime here to init the Persistent value, making this slightly easier to digest for the reader. elexis: One doesn't have to Root the object to test if it is a function. This means we only need the… | |||||
LoadPage(m_PageStack.back()); | |||||
if (JS_ObjectIsFunction(cxSrc, callbackObj)) | |||||
{ | |||||
m_PageStack.back().callbackFunction.reset(); | |||||
m_PageStack.back().callbackFunction.init(srcScriptInterface->GetJSRuntime(), callbackFunction); | |||||
} | |||||
} | |||||
m_PageStack.emplace_back(SGUIPage(pageName, initData, m_ScriptRuntime)); | |||||
ResetCursor(); | ResetCursor(); | ||||
} | } | ||||
void CGUIManager::PopPage() | void CGUIManager::PopPage(shared_ptr<ScriptInterface::StructuredClone> args) | ||||
{ | { | ||||
if (m_PageStack.size() < 2) | if (m_PageStack.size() < 2) | ||||
{ | { | ||||
debug_warn(L"Tried to pop GUI page when there's < 2 in the stack"); | LOGWARNING("Tried to pop GUI page when there's < 2 in the stack"); | ||||
return; | return; | ||||
} | } | ||||
m_PageStack.pop_back(); | m_PageStack.pop_back(); | ||||
} | |||||
void CGUIManager::PopPageCB(shared_ptr<ScriptInterface::StructuredClone> args) | if (m_PageStack.back().callbackFunction.isUndefined()) | ||||
{ | return; | ||||
shared_ptr<ScriptInterface::StructuredClone> initDataClone = m_PageStack.back().initData; | |||||
PopPage(); | |||||
shared_ptr<ScriptInterface> scriptInterface = m_PageStack.back().gui->GetScriptInterface(); | shared_ptr<ScriptInterface> scriptInterface = top()->GetScriptInterface(); | ||||
JSContext* cx = scriptInterface->GetContext(); | JSContext* cx = scriptInterface->GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
JS::RootedValue initDataVal(cx); | JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | ||||
if (!initDataClone) | JS::RootedValue argVal(cx); | ||||
{ | if (args) | ||||
LOGERROR("Called PopPageCB when initData (which should contain the callback function name) isn't set!"); | scriptInterface->ReadStructuredClone(args, &argVal); | ||||
return; | |||||
} | |||||
scriptInterface->ReadStructuredClone(initDataClone, &initDataVal); | |||||
if (!scriptInterface->HasProperty(initDataVal, "callback")) | JS::RootedObject globalObj(cx, &global.toObject()); | ||||
{ | JS::RootedValue funcVal(cx, m_PageStack.back().callbackFunction); | ||||
LOGERROR("Called PopPageCB when the callback function name isn't set!"); | m_PageStack.back().callbackFunction.reset(); | ||||
Not Done Inline ActionsWhy the cleanup? wraitii: Why the cleanup? | |||||
Done Inline ActionsI remember bruteforce-coding this reset calls as it kept segfaulting for days. I have to check again. elexis: I remember bruteforce-coding this reset calls as it kept segfaulting for days. I have to check… | |||||
return; | m_PageStack.back().callbackFunction.init(m_ScriptRuntime->m_rt, JS::UndefinedValue()); | ||||
JS::AutoValueVector paramData(cx); | |||||
paramData.append(argVal); | |||||
JS::RootedValue result(cx); | |||||
JS_CallFunctionValue(cx, globalObj, funcVal, paramData, &result); | |||||
} | } | ||||
std::string callback; | CGUIManager::SGUIPage::SGUIPage(const CStrW& pageName, const shared_ptr<ScriptInterface::StructuredClone> initData, shared_ptr<ScriptRuntime> scriptRuntime) | ||||
if (!scriptInterface->GetProperty(initDataVal, "callback", callback)) | : name(pageName), initData(initData), inputs(), gui(), callbackFunction() | ||||
{ | { | ||||
LOGERROR("Failed to get the callback property as a string from initData in PopPageCB!"); | // TODO: Should this be moved to LoadPage? | ||||
return; | callbackFunction.reset(); | ||||
} | callbackFunction.init(scriptRuntime->m_rt, JS::UndefinedValue()); | ||||
JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | LoadPage(scriptRuntime); | ||||
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); | callbackFunction.reset(); | ||||
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; | |||||
} | |||||
} | } | ||||
void CGUIManager::LoadPage(SGUIPage& page) | void CGUIManager::SGUIPage::LoadPage(shared_ptr<ScriptRuntime> scriptRuntime) | ||||
{ | { | ||||
// If we're hotloading then try to grab some data from the previous page | // If we're hotloading then try to grab some data from the previous page | ||||
shared_ptr<ScriptInterface::StructuredClone> hotloadData; | shared_ptr<ScriptInterface::StructuredClone> hotloadData; | ||||
if (page.gui) | if (gui) | ||||
{ | { | ||||
shared_ptr<ScriptInterface> scriptInterface = page.gui->GetScriptInterface(); | shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface(); | ||||
JSContext* cx = scriptInterface->GetContext(); | JSContext* cx = scriptInterface->GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | ||||
JS::RootedValue hotloadDataVal(cx); | JS::RootedValue hotloadDataVal(cx); | ||||
scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal); | scriptInterface->CallFunction(global, "getHotloadData", &hotloadDataVal); | ||||
hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal); | hotloadData = scriptInterface->WriteStructuredClone(hotloadDataVal); | ||||
} | } | ||||
page.inputs.clear(); | inputs.clear(); | ||||
page.gui.reset(new CGUI(m_ScriptRuntime)); | gui.reset(new CGUI(scriptRuntime)); | ||||
page.gui->Initialize(); | gui->Initialize(); | ||||
VfsPath path = VfsPath("gui") / page.name; | VfsPath path = VfsPath("gui") / name; | ||||
page.inputs.insert(path); | inputs.insert(path); | ||||
CXeromyces xero; | CXeromyces xero; | ||||
if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK) | if (xero.Load(g_VFS, path, "gui_page") != PSRETURN_OK) | ||||
// Fail silently (Xeromyces reported the error) | // Fail silently (Xeromyces reported the error) | ||||
return; | return; | ||||
int elmt_page = xero.GetElementID("page"); | int elmt_page = xero.GetElementID("page"); | ||||
int elmt_include = xero.GetElementID("include"); | int elmt_include = xero.GetElementID("include"); | ||||
XMBElement root = xero.GetRoot(); | XMBElement root = xero.GetRoot(); | ||||
if (root.GetNodeName() != elmt_page) | if (root.GetNodeName() != elmt_page) | ||||
{ | { | ||||
LOGERROR("GUI page '%s' must have root element <page>", utf8_from_wstring(page.name)); | LOGERROR("GUI page '%s' must have root element <page>", utf8_from_wstring(name)); | ||||
return; | return; | ||||
} | } | ||||
XERO_ITER_EL(root, node) | XERO_ITER_EL(root, node) | ||||
{ | { | ||||
if (node.GetNodeName() != elmt_include) | if (node.GetNodeName() != elmt_include) | ||||
{ | { | ||||
LOGERROR("GUI page '%s' must only have <include> elements inside <page>", utf8_from_wstring(page.name)); | LOGERROR("GUI page '%s' must only have <include> elements inside <page>", utf8_from_wstring(name)); | ||||
continue; | continue; | ||||
} | } | ||||
std::string name = node.GetText(); | std::string name = node.GetText(); | ||||
CStrW nameW (node.GetText().FromUTF8()); | CStrW nameW (node.GetText().FromUTF8()); | ||||
PROFILE2("load gui xml"); | PROFILE2("load gui xml"); | ||||
PROFILE2_ATTR("name: %s", name.c_str()); | PROFILE2_ATTR("name: %s", name.c_str()); | ||||
TIMER(nameW.c_str()); | TIMER(nameW.c_str()); | ||||
if (name.back() == '/') | if (name.back() == '/') | ||||
{ | { | ||||
VfsPath directory = VfsPath("gui") / nameW; | VfsPath directory = VfsPath("gui") / nameW; | ||||
VfsPaths pathnames; | VfsPaths pathnames; | ||||
vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames); | vfs::GetPathnames(g_VFS, directory, L"*.xml", pathnames); | ||||
for (const VfsPath& path : pathnames) | for (const VfsPath& path : pathnames) | ||||
page.gui->LoadXmlFile(path, page.inputs); | gui->LoadXmlFile(path, inputs); | ||||
} | } | ||||
else | else | ||||
{ | { | ||||
VfsPath path = VfsPath("gui") / nameW; | VfsPath path = VfsPath("gui") / nameW; | ||||
page.gui->LoadXmlFile(path, page.inputs); | gui->LoadXmlFile(path, inputs); | ||||
} | } | ||||
} | } | ||||
page.gui->SendEventToAll("load"); | gui->SendEventToAll("load"); | ||||
shared_ptr<ScriptInterface> scriptInterface = page.gui->GetScriptInterface(); | shared_ptr<ScriptInterface> scriptInterface = gui->GetScriptInterface(); | ||||
JSContext* cx = scriptInterface->GetContext(); | JSContext* cx = scriptInterface->GetContext(); | ||||
JSAutoRequest rq(cx); | JSAutoRequest rq(cx); | ||||
JS::RootedValue initDataVal(cx); | JS::RootedValue initDataVal(cx); | ||||
JS::RootedValue hotloadDataVal(cx); | JS::RootedValue hotloadDataVal(cx); | ||||
JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); | ||||
if (page.initData) | if (initData) | ||||
scriptInterface->ReadStructuredClone(page.initData, &initDataVal); | scriptInterface->ReadStructuredClone(initData, &initDataVal); | ||||
if (hotloadData) | if (hotloadData) | ||||
scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal); | scriptInterface->ReadStructuredClone(hotloadData, &hotloadDataVal); | ||||
if (scriptInterface->HasProperty(global, "init") && | if (scriptInterface->HasProperty(global, "init") && | ||||
!scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal)) | !scriptInterface->CallFunctionVoid(global, "init", initDataVal, hotloadDataVal)) | ||||
LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(page.name)); | LOGERROR("GUI page '%s': Failed to call init() function", utf8_from_wstring(name)); | ||||
} | } | ||||
Status CGUIManager::ReloadChangedFile(const VfsPath& path) | Status CGUIManager::ReloadChangedFile(const VfsPath& path) | ||||
{ | { | ||||
for (SGUIPage& p : m_PageStack) | for (SGUIPage& p : m_PageStack) | ||||
if (p.inputs.count(path)) | if (p.inputs.count(path)) | ||||
{ | { | ||||
LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.name)); | LOGMESSAGE("GUI file '%s' changed - reloading page '%s'", path.string8(), utf8_from_wstring(p.name)); | ||||
LoadPage(p); | p.LoadPage(m_ScriptRuntime); | ||||
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators | // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators | ||||
} | } | ||||
return INFO::OK; | return INFO::OK; | ||||
} | } | ||||
Status CGUIManager::ReloadAllPages() | Status CGUIManager::ReloadAllPages() | ||||
{ | { | ||||
// TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators | // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators | ||||
for (SGUIPage& p : m_PageStack) | for (SGUIPage& p : m_PageStack) | ||||
LoadPage(p); | p.LoadPage(m_ScriptRuntime); | ||||
return INFO::OK; | return INFO::OK; | ||||
} | } | ||||
void CGUIManager::ResetCursor() | void CGUIManager::ResetCursor() | ||||
{ | { | ||||
g_CursorName = g_DefaultCursor; | g_CursorName = g_DefaultCursor; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 60 Lines • ▼ Show 20 Lines | bool handled; | ||||
if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev)) | if (top()->GetScriptInterface()->CallFunction(global, "handleInputAfterGui", handled, *ev)) | ||||
if (handled) | if (handled) | ||||
return IN_HANDLED; | return IN_HANDLED; | ||||
} | } | ||||
return IN_PASS; | return IN_PASS; | ||||
} | } | ||||
bool CGUIManager::GetPreDefinedColor(const CStr& name, CColor& output) const | |||||
{ | |||||
return top()->GetPreDefinedColor(name, output); | |||||
} | |||||
void CGUIManager::SendEventToAll(const CStr& eventName) const | void CGUIManager::SendEventToAll(const CStr& eventName) const | ||||
{ | { | ||||
top()->SendEventToAll(eventName); | top()->SendEventToAll(eventName); | ||||
} | } | ||||
void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const | void CGUIManager::SendEventToAll(const CStr& eventName, JS::HandleValueArray paramData) const | ||||
{ | { | ||||
top()->SendEventToAll(eventName, paramData); | top()->SendEventToAll(eventName, paramData); | ||||
▲ Show 20 Lines • Show All 59 Lines • Show Last 20 Lines |
Why different runtimes?