Rework the logics of options loading to allow more flexibility (i.e. adding a min or max value for number inputs)

Fix some problems when reloading saved options, refs #3511
Add the material quality option, refs #3737
Reload and Save buttons are only enabled when some options have changed.

This was SVN commit r17645.
This commit is contained in:
mimo 2016-01-13 22:01:36 +00:00
parent cd22f9f722
commit 5c61f68600
3 changed files with 109 additions and 78 deletions

View File

@ -1,5 +1,5 @@
var g_hasCallback = false;
var g_hasChanges = false;
var g_controls;
/**
* This array holds the data to populate the general section with.
* Data is in the form [Title, Tooltip, {ActionType:Action}, InputType].
@ -35,18 +35,19 @@ var options = {
[translate("Shadows on Water"), translate("Cast shadows on water"), {"renderer":"WaterShadows", "config":"watershadows"}, "boolean"],
[translate("VSync"), translate("Run vertical sync to fix screen tearing. REQUIRES GAME RESTART"), {"config":"vsync"}, "boolean"],
[translate("Limit FPS in Menus"), translate("Limit FPS to 50 in all menus, to save power."), {"config":"gui.menu.limitfps"}, "boolean"],
[translate("Graphics quality"), translate("Graphics quality. REQUIRES GAME RESTART"), {"config":"materialmgr.quality", "min": "0", "max": "10"}, "number"],
],
"soundSetting":
[
[translate("Master Gain"), translate("Master audio gain"), {"config":"sound.mastergain", "function":"Engine.SetMasterGain(Number(this.caption));"}, "number"],
[translate("Music Gain"), translate("In game music gain"), {"config":"sound.musicgain", "function":"Engine.SetMusicGain(Number(this.caption));"}, "number"],
[translate("Ambient Gain"), translate("In game ambient sound gain"), {"config":"sound.ambientgain", "function":"Engine.SetAmbientGain(Number(this.caption));"}, "number"],
[translate("Action Gain"), translate("In game unit action sound gain"), {"config":"sound.actiongain", "function":"Engine.SetActionGain(Number(this.caption));"}, "number"],
[translate("UI Gain"), translate("UI sound gain"), {"config":"sound.uigain", "function":"Engine.SetUIGain(Number(this.caption));"}, "number"],
[translate("Master Gain"), translate("Master audio gain"), {"config":"sound.mastergain", "function":"Engine.SetMasterGain(Number(this.caption));", "min": "0"}, "number"],
[translate("Music Gain"), translate("In game music gain"), {"config":"sound.musicgain", "function":"Engine.SetMusicGain(Number(this.caption));", "min": "0"}, "number"],
[translate("Ambient Gain"), translate("In game ambient sound gain"), {"config":"sound.ambientgain", "function":"Engine.SetAmbientGain(Number(this.caption));", "min": "0"}, "number"],
[translate("Action Gain"), translate("In game unit action sound gain"), {"config":"sound.actiongain", "function":"Engine.SetActionGain(Number(this.caption));", "min": "0"}, "number"],
[translate("UI Gain"), translate("UI sound gain"), {"config":"sound.uigain", "function":"Engine.SetUIGain(Number(this.caption));", "min": "0"}, "number"],
],
"lobbySetting":
[
[translate("Chat Backlog"), translate("Number of backlogged messages to load when joining the lobby"), {"config":"lobby.history"}, "number"],
[translate("Chat Backlog"), translate("Number of backlogged messages to load when joining the lobby"), {"config":"lobby.history", "min": "0"}, "number"],
[translate("Chat Timestamp"), translate("Show time that messages are posted in the lobby chat"), {"config":"lobby.chattimestamp"}, "boolean"],
],
};
@ -56,6 +57,7 @@ function init(data)
if (data && data.callback)
g_hasCallback = true;
let reload = data && data.reload;
g_controls = [];
// WARNING: We assume a strict formatting of the XML and do minimal checking.
for (let prefix of Object.keys(options))
@ -87,6 +89,9 @@ function init(data)
body.hidden = false;
}
}
if (!reload)
updateStatus();
}
/**
@ -98,7 +103,7 @@ function init(data)
function setupControl(option, i, prefix, reload)
{
var control;
var onPress = function(){ g_hasChanges = true; };
var onPress;
switch (option[3])
{
@ -110,56 +115,51 @@ function setupControl(option, i, prefix, reload)
text.size = size;
control = Engine.GetGUIObjectByName(prefix + "Tickbox[" + i + "]");
var checked;
// Different option action load and save differently, so this switch is needed.
var keyRenderer;
var keyConfig;
var functionBody;
for (let action of Object.keys(option[2]))
{
switch (action)
{
case "config":
// Load initial value if not yet loaded.
if (!checked || typeof checked != "boolean")
checked = Engine.ConfigDB_GetValue("user", option[2][action]) === "true";
// Hacky macro to create the callback.
var callback = function(key)
{
return function()
Engine.ConfigDB_CreateValue("user", key, String(this.checked));
}(option[2][action]);
// Merge the new callback with any existing callbacks.
onPress = mergeFunctions(callback, onPress);
keyConfig = option[2].config;
if (checked === undefined || reload)
checked = Engine.ConfigDB_GetValue("user", keyConfig) === "true";
else if ((Engine.ConfigDB_GetValue("user", keyConfig) === "true") !== checked)
Engine.ConfigDB_CreateValue("user", keyConfig, String(checked));
break;
case "renderer":
// If reloading, config values have priority, otherwise load initial value if not yet loaded
if (reload && option[2].config)
{
checked = Engine.ConfigDB_GetValue("user", option[2].config) === "true";
let rendererChecked = eval("Engine.Renderer_Get" + option[2].renderer + "Enabled()");
if (rendererChecked != checked)
eval("Engine.Renderer_Set" + option[2].renderer + "Enabled(" + checked + ")");
}
else if (!checked || typeof checked != "boolean")
checked = eval("Engine.Renderer_Get" + option[2][action] + "Enabled()");
// Hacky macro to create the callback (updating also the config value if any).
var callback = function(key, keyConfig)
{
return function()
{
eval("Engine.Renderer_Set" + key + "Enabled(" + this.checked + ")");
if (keyConfig)
Engine.ConfigDB_CreateValue("user", keyConfig, String(this.checked));
};
}(option[2][action], option[2].config);
// Merge the new callback with any existing callbacks.
onPress = mergeFunctions(callback, onPress);
keyRenderer = option[2].renderer;
if (checked === undefined)
checked = eval("Engine.Renderer_Get" + keyRenderer + "Enabled()");
else if (eval("Engine.Renderer_Get" + keyRenderer + "Enabled()") !== checked)
eval("Engine.Renderer_Set" + keyRenderer + "Enabled(" + checked + ")");
break;
case "function":
// This allows for doing low-level actions, like hiding/showing UI elements.
onPress = mergeFunctions(eval("function(){" + option[2][action] + "}"), onPress);
functionBody = option[2].function;
break;
default:
warn("Unknown option source type '" + action + "'");
}
}
onPress = function(keyRenderer, keyConfig, functionBody)
{
return function()
{
if (keyRenderer)
eval("Engine.Renderer_Set" + keyRenderer + "Enabled(" + this.checked + ")");
if (keyConfig)
Engine.ConfigDB_CreateValue("user", keyConfig, String(this.checked));
if (functionBody)
eval(functionBody);
updateStatus(true);
};
}(keyRenderer, keyConfig, functionBody);
// Load final data to the control element.
control.checked = checked;
control.onPress = onPress;
@ -169,41 +169,62 @@ function setupControl(option, i, prefix, reload)
case "string":
control = Engine.GetGUIObjectByName(prefix + "Input[" + i + "]");
var caption;
var key;
var functionBody;
var minval;
var maxval;
for (let action of Object.keys(option[2]))
{
switch (action)
{
case "config":
onPress = function(){};
caption = Engine.ConfigDB_GetValue("user", option[2][action]);
// Hacky macro to create the callback.
var callback = function(key)
{
return function()
{
if (Engine.ConfigDB_GetValue("user", key) == this.caption)
return;
Engine.ConfigDB_CreateValue("user", key, String(this.caption));
g_hasChanges = true;
};
}(option[2][action]);
// Merge the new callback with any existing callbacks.
onPress = mergeFunctions(callback, onPress);
key = option[2].config;
caption = Engine.ConfigDB_GetValue("user", key);
break;
case "function":
// This allows for doing low-level actions, like hiding/showing UI elements.
onPress = mergeFunctions(function(){eval(option[2][action]);}, onPress);
functionBody = option[2].function;
break;
case "min":
minval = option[2].min;
break;
case "max":
maxval = option[2].max;
break;
default:
warn("Unknown option source type '" + action + "'");
}
}
// as the enter key is not necessarily pressed after modifying an entry, we will register the input also
// - when the mouse leave the control (MouseLeave event)
// - or when saving or closing the window (registerChanges function)
// so we must ensure that something has indeed been modified
onPress = function(key, functionBody, minval, maxval)
{
return function()
{
if (minval && +minval > +this.caption)
this.caption = minval;
if (maxval && +maxval < +this.caption)
this.caption = maxval;
if (Engine.ConfigDB_GetValue("user", key) === this.caption)
return;
Engine.ConfigDB_CreateValue("user", key, this.caption);
if (functionBody)
eval(functionBody);
updateStatus(true);
};
}(key, functionBody, minval, maxval);
control.caption = caption;
control.onPress = onPress;
control.onMouseLeave = onPress;
g_controls.push(control);
break;
default:
warn("Unknown option type '" + options[3] + "', assuming string. Valid types are 'number', 'string', or 'bool'.");
warn("Unknown option type '" + option[3] + "', assuming string. Valid types are 'number', 'string', or 'bool'.");
control = Engine.GetGUIObjectByName(prefix + "Input[" + i + "]");
break;
}
@ -212,31 +233,38 @@ function setupControl(option, i, prefix, reload)
return control;
}
/**
* Merge two functions which don't expect arguments.
*
* @return Merged function.
*/
function mergeFunctions(function1, function2)
function updateStatus(val)
{
return function()
{
function1.apply(this);
function2.apply(this);
};
if (typeof val == "boolean")
Engine.ConfigDB_CreateValue("user", "nosave.haschanges", String(val));
else
val = Engine.ConfigDB_GetValue("user", "nosave.haschanges") === "true";
Engine.GetGUIObjectByName("loadOptions").enabled = val;
Engine.GetGUIObjectByName("saveOptions").enabled = val;
}
/**
* Register changes of input (text and number) controls
*/
function registerChanges()
{
for (let control of g_controls)
control.onPress();
}
function reloadDefaults()
{
Engine.ConfigDB_Reload("user");
updateStatus(false);
init({ "reload": true });
g_hasChanges = false;
}
function saveDefaults()
{
g_hasChanges = false;
registerChanges();
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
updateStatus(false);
}
/**
@ -244,7 +272,8 @@ function saveDefaults()
**/
function closePage()
{
if (g_hasChanges)
registerChanges();
if (Engine.ConfigDB_GetValue("user", "nosave.haschanges") === "true")
{
let btCaptions = [translate("No"), translate("Yes")];
let btCode = [null, function(){ closePageWithoutConfirmation(); }];

View File

@ -65,14 +65,14 @@
</object>
</repeat>
</object>
<object type="button" style="ModernButtonRed" size="50%-170 100%-44 50%-70 100%-16" hotkey="cancel">
<object name="loadOptions" type="button" style="ModernButtonRed" size="50%-170 100%-44 50%-70 100%-16" hotkey="cancel">
<translatableAttribute id="caption">Reload</translatableAttribute>
<translatableAttribute id="tooltip">Reload default values from file</translatableAttribute>
<translatableAttribute id="tooltip">Reload previous saved settings</translatableAttribute>
<action on="Press">reloadDefaults();</action>
</object>
<object type="button" style="ModernButtonRed" size="50%-62 100%-44 50%+38 100%-16">
<object name="saveOptions" type="button" style="ModernButtonRed" size="50%-62 100%-44 50%+38 100%-16">
<translatableAttribute id="caption">Save</translatableAttribute>
<translatableAttribute id="tooltip">Save changes to file</translatableAttribute>
<translatableAttribute id="tooltip">Save changes</translatableAttribute>
<action on="Press">saveDefaults();</action>
</object>
<object type="button" style="ModernButtonRed" size="50%+70 100%-44 50%+170 100%-16">

View File

@ -378,6 +378,8 @@ bool CConfigDB::WriteFile(EConfigNamespace ns, const VfsPath& path) const
char* pos = (char*)buf.get();
for (const std::pair<CStr, CConfigValueSet>& p : m_Map[ns])
{
if (boost::algorithm::starts_with(p.first, "nosave."))
continue;
size_t i;
pos += sprintf(pos, "%s = ", p.first.c_str());
for (i = 0; i < p.second.size() - 1; ++i)