Use tabs for the options page and rewrite it altogether.

Removes a lot of JS and XML duplication, unneeded for-loops.
Display min/max in the tooltip for number inputs.
Increase the width for dropdowns and dynamically maximize the width of
the label.
Remember the selected tab when hotloading the page.

Differential Revision: https://code.wildfiregames.com/D805
Refs #3737
Graphical design agreed with: scythetwirler, mimo, Itms, fpre, Imarok,
bb, leper, brian, feneur, implodedok
Testing By: Vladislav, mimo
This was SVN commit r20101.
This commit is contained in:
elexis
2017-09-03 21:38:50 +00:00
parent 9cb0a60d73
commit dd7f38e370
3 changed files with 318 additions and 352 deletions
+264 -274
View File
@@ -1,285 +1,280 @@
var g_HasCallback = false;
var g_Controls;
/**
* Translated JSON file contents.
*/
var g_Options;
/**
* Numerical index of the chosen category.
*/
var g_SelectedCategory;
/**
* Remember whether to unpause running singleplayer games.
*/
var g_HasCallback;
/**
* Vertical size of a tab button.
*/
var g_TabButtonHeight = 30;
/**
* Vertical space between two tab buttons.
*/
var g_TabButtonDist = 5;
/**
* Vertical distance between the top of the page and the first option.
*/
var g_OptionControlOffset = 5;
/**
* Vertical size of each option control.
*/
var g_OptionControlHeight = 26;
/**
* Vertical distance between two consecutive options.
*/
var g_OptionControlDist = 2;
/**
* Horizontal indentation to distinguish options that depend on another option.
*/
var g_DependentLabelIndentation = 25;
function init(data)
{
if (data && data.callback)
g_HasCallback = true;
g_Controls = {};
let options = Engine.ReadJSONFile("gui/options/options.json");
for (let category in options)
/**
* Defines the parsing of config strings and GUI control interaction for the different option types.
*
* @property configToValue - parses a string from the user config to a value of the declared type.
* @property valueToGui - sets the GUI control to display the given value.
* @property guiToValue - returns the value of the GUI control.
* @property guiSetter - event name that should be considered a value change of the GUI control.
* @property initGUI - sets properties of the GUI control that are independent of the current value.
* @property sanitizeValue - ensures that a given value is in the defined value range.
* @property tooltip - appends a custom tooltip to the given option description depending on the current value.
*/
var g_OptionType = {
"boolean":
{
let lastSize;
for (let i = 0; i < options[category].length; ++i)
{
let option = options[category][i];
if (!option.label || !option.parameters || !option.parameters.config)
continue;
let body = Engine.GetGUIObjectByName(category + "[" + i + "]");
let label = Engine.GetGUIObjectByName(category + "Label[" + i + "]");
let config = option.parameters.config;
g_Controls[config] = {
"control": setupControl(option, i, category),
"label": label,
"type": option.type,
"dependencies": option.dependencies || undefined,
"parameters": option.parameters
};
label.caption = translate(option.label);
label.tooltip = option.tooltip ? translate(option.tooltip) : "";
// Move each element to the correct place.
if (lastSize)
{
let newSize = new GUISize();
newSize.left = lastSize.left;
newSize.rright = lastSize.rright;
newSize.top = lastSize.bottom;
newSize.bottom = newSize.top + 26;
body.size = newSize;
lastSize = newSize;
}
else
lastSize = body.size;
let labelSize = label.size;
labelSize.left = option.dependency ? g_DependentLabelIndentation : 0;
labelSize.rright = g_Controls[config].control.size.rleft;
label.size = labelSize;
body.hidden = false;
}
"configToValue": config => config == "true",
"valueToGui": (value, control) => {
control.checked = value;
},
"guiToValue": control => control.checked,
"guiSetter": "onPress"
},
"string":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => control.caption,
"guiSetter": "onTextEdit"
},
"number":
{
"configToValue": value => +value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => +control.caption,
"guiSetter": "onTextEdit",
"sanitizeValue": (value, option) =>
Math.min(option.parameters.max || Infinity,
Math.max(option.parameters.min || -Infinity, value)),
"tooltip": (value, option) =>
sprintf(
option.parameters.min !== undefined && option.parameters.max !== undefined ?
translateWithContext("option number", "Min: %(min)s, Max: %(max)s") :
option.parameters.min !== undefined && option.parameters.max === undefined ?
translateWithContext("option number", "Min: %(min)s") :
option.parameters.min === undefined && option.parameters.max !== undefined ?
translateWithContext("option number", "Max: %(max)s") :
"",
{
"min": option.parameters.min,
"max": option.parameters.max
})
},
"dropdown":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.selected = control.list_data.indexOf(value);
},
"guiToValue": control => control.list_data[control.selected],
"guiSetter": "onSelectionChange",
"initGUI": (option, control) => {
control.list = option.parameters.list.map(e => e.label);
control.list_data = option.parameters.list.map(e => e.value);
},
},
"slider":
{
"configToValue": value => +value,
"valueToGui": (value, control) => {
control.value = +value;
},
"guiToValue": control => control.value,
"guiSetter": "onValueChange",
"initGUI": (option, control) => {
control.max_value = option.parameters.max;
control.min_value = option.parameters.min;
},
"tooltip": (value, option) =>
sprintf(translateWithContext("slider number", "Value: %(val)s (min: %(min)s, max: %(max)s)"), {
"val": value.toFixed(2),
"min": option.parameters.min.toFixed(2),
"max": option.parameters.max.toFixed(2)
})
}
};
updateOptionPanel();
function init(data, hotloadData)
{
g_HasCallback = hotloadData && hotloadData.callback || data && data.callback;
g_SelectedCategory = hotloadData ? hotloadData.selectedCategory : 0;
g_Options = Engine.ReadJSONFile("gui/options/options.json");
translateObjectKeys(g_Options, ["label", "tooltip"]);
deepfreeze(g_Options);
placeTabButtons();
displayOptions();
}
function getHotloadData()
{
return {
"selectedCategory": g_SelectedCategory,
"callback": g_HasCallback
};
}
function placeTabButtons()
{
for (let category in g_Options)
{
let button = Engine.GetGUIObjectByName("tabButton[" + category + "]");
if (!button)
{
warn("Too few tab-buttons!");
break;
}
button.hidden = false;
let size = button.size;
size.top = category * (g_TabButtonHeight + g_TabButtonDist);
size.bottom = size.top + g_TabButtonHeight;
button.size = size;
button.tooltip = g_Options[category].tooltip || "";
button.onPress = (category => function() {
g_SelectedCategory = category;
displayOptions();
})(category);
Engine.GetGUIObjectByName("tabButtonText[" + category + "]").caption = g_Options[category].label;
}
}
/**
* Setup the apropriate control for a given option.
*
* @param option Structure containing the data to setup an option.
* @param prefix Prefix to use when accessing control, for example "generalSetting" when the tickbox name is generalSettingTickbox[i].
* Sets up labels and controls of all options of the currently selected category.
*/
function setupControl(option, i, category)
function displayOptions()
{
let control;
let onUpdate;
let key = option.parameters.config;
// Highlight the selected tab
Engine.GetGUIObjectByName("tabButtons").children.forEach((button, i) => {
button.sprite = i == g_SelectedCategory ? "ModernTabVerticalForeground" : "ModernTabVerticalBackground";
});
switch (option.type)
// Hide all controls
for (let body of Engine.GetGUIObjectByName("option_controls").children)
{
case "boolean":
control = Engine.GetGUIObjectByName(category + "Tickbox[" + i + "]");
let checked;
for (let param in option.parameters)
{
switch (param)
{
case "config":
checked = Engine.ConfigDB_GetValue("user", key) == "true";
break;
case "function":
break;
default:
warn("Unknown option source type '" + param + "'");
}
}
body.hidden = true;
for (let control of body.children)
control.hidden = true;
}
onUpdate = function(key)
{
return function()
{
if (option.parameters.function)
Engine[option.parameters.function](this.checked);
Engine.ConfigDB_CreateValue("user", key, String(this.checked));
Engine.ConfigDB_SetChanges("user", true);
updateOptionPanel();
};
}(key);
// Load final data to the control element.
control.checked = checked;
control.onPress = onUpdate;
break;
case "slider":
control = Engine.GetGUIObjectByName(category + "Slider[" + i + "]");
let value;
let callbackFunction;
let minvalue;
let maxvalue;
for (let param in option.parameters)
{
switch (param)
{
case "config":
value = +Engine.ConfigDB_GetValue("user", key);
break;
case "function":
if (Engine[option.parameters.function])
callbackFunction = option.parameters.function;
break;
case "min":
minvalue = +option.parameters.min;
break;
case "max":
maxvalue = +option.parameters.max;
break;
default:
warn("Unknown option source type '" + param + "'");
}
}
onUpdate = function(key, callbackFunction, minvalue, maxvalue)
{
return function()
{
this.tooltip =
(option.tooltip ? translate(option.tooltip) + "\n" : "") +
sprintf(translateWithContext("slider number", "Value: %(val)s (min: %(min)s, max: %(max)s)"), {
"val": +this.value.toFixed(2),
"min": +minvalue.toFixed(2),
"max": +maxvalue.toFixed(2)
});
if (+Engine.ConfigDB_GetValue("user", key) === this.value)
return;
Engine.ConfigDB_CreateValue("user", key, this.value);
Engine.ConfigDB_SetChanges("user", true);
if (callbackFunction)
Engine[callbackFunction](+this.value);
updateOptionPanel();
};
}(key, callbackFunction, minvalue, maxvalue);
control.value = value;
control.max_value = maxvalue;
control.min_value = minvalue;
control.onValueChange = onUpdate;
break;
case "number":
case "string":
control = Engine.GetGUIObjectByName(category + "Input[" + i + "]");
let caption;
let functionBody;
let minval;
let maxval;
for (let param in option.parameters)
{
switch (param)
{
case "config":
caption = Engine.ConfigDB_GetValue("user", key);
break;
case "function":
if (Engine[option.parameters.function])
functionBody = option.parameters.function;
break;
case "min":
minval = option.parameters.min;
break;
case "max":
maxval = option.parameters.max;
break;
default:
warn("Unknown option source type '" + param + "'");
}
}
onUpdate = 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);
Engine.ConfigDB_SetChanges("user", true);
if (functionBody)
Engine[functionBody](+this.caption);
updateOptionPanel();
};
}(key, functionBody, minval, maxval);
control.caption = caption;
control.onTextEdit = onUpdate;
break;
case "dropdown":
// Initialize label and control of each option for this category
for (let i = 0; i < g_Options[g_SelectedCategory].options.length; ++i)
{
control = Engine.GetGUIObjectByName(category + "Dropdown[" + i + "]");
control.onSelectionChange = function(){}; // just the time to setup the value
let config;
let callbackFunction;
// Position vertically
let body = Engine.GetGUIObjectByName("option_control[" + i + "]");
let bodySize = body.size;
bodySize.top = g_OptionControlOffset + i * (g_OptionControlHeight + g_OptionControlDist);
bodySize.bottom = bodySize.top + g_OptionControlHeight;
body.size = bodySize;
body.hidden = false;
for (let param in option.parameters)
{
switch (param)
// Load option data
let option = g_Options[g_SelectedCategory].options[i];
let optionType = g_OptionType[option.type];
let value = optionType.configToValue(Engine.ConfigDB_GetValue("user", option.parameters.config));
// Setup control
let control = Engine.GetGUIObjectByName("option_control_" + option.type + "[" + i + "]");
control.tooltip = option.tooltip + "\n" + (optionType.tooltip && optionType.tooltip(value, option));
control.hidden = false;
if (optionType.initGUI)
optionType.initGUI(option, control);
control[optionType.guiSetter] = function() {};
optionType.valueToGui(value, control);
control[optionType.guiSetter] = function() {
let value = optionType.guiToValue(control);
if (optionType.sanitizeValue)
{
case "config":
config = Engine.ConfigDB_GetValue("user", key);
break;
case "function":
if (Engine[option.parameters.function])
callbackFunction = option.parameters.function;
break;
case "list":
control.list = option.parameters.list.map(e => translate(e.label));
let values = option.parameters.list.map(e => e.value);
control.list_data = values;
control.selected = values.map(String).indexOf(config);
break;
default:
warn("Unknown option source type '" + param + "'");
value = optionType.sanitizeValue(value, option);
optionType.valueToGui(value, control);
}
}
onUpdate = function(key, callbackFunction)
{
return function()
{
Engine.ConfigDB_CreateValue("user", key, this.list_data[this.selected]);
Engine.ConfigDB_SetChanges("user", true);
if (callbackFunction)
Engine[callbackFunction](this.list_data[this.selected]);
updateOptionPanel();
};
}(key, callbackFunction);
control.tooltip = option.tooltip + "\n" + (optionType.tooltip && optionType.tooltip(value, option));
control.onSelectionChange = onUpdate;
break;
Engine.ConfigDB_CreateValue("user", option.parameters.config, String(value));
Engine.ConfigDB_SetChanges("user", true);
if (option.parameters.function)
Engine[option.parameters.function](value);
enableButtons();
};
// Setup label
let label = Engine.GetGUIObjectByName("option_label[" + i + "]");
label.caption = option.label;
label.tooltip = option.tooltip;
label.hidden = false;
let labelSize = label.size;
labelSize.left = option.dependencies ? g_DependentLabelIndentation : 0;
labelSize.rright = control.size.rleft;
label.size = labelSize;
}
default:
warn("Unknown option type " + option.type + ", assuming string.");
control = Engine.GetGUIObjectByName(category + "Input[" + i + "]");
break;
}
control.hidden = false;
if (option.type == "slider")
control.onValueChange();
else
control.tooltip = option.tooltip ? translate(option.tooltip) : "";
return control;
enableButtons();
}
function updateOptionPanel()
/**
* Enable exactly the buttons whose dependencies are met.
*/
function enableButtons()
{
for (let item in g_Controls)
{
let enabled =
!g_Controls[item].dependencies ||
g_Controls[item].dependencies.every(config => Engine.ConfigDB_GetValue("user", config) == "true");
g_Options[g_SelectedCategory].options.forEach((option, i) => {
g_Controls[item].control.enabled = enabled;
g_Controls[item].label.enabled = enabled;
}
let enabled =
!option.dependencies ||
option.dependencies.every(config => Engine.ConfigDB_GetValue("user", config) == "true");
Engine.GetGUIObjectByName("option_label[" + i + "]").enabled = enabled;
Engine.GetGUIObjectByName("option_control_" + option.type + "[" + i + "]").enabled = enabled;
});
let hasChanges = Engine.ConfigDB_HasChanges("user");
Engine.GetGUIObjectByName("revertChanges").enabled = hasChanges;
@@ -299,8 +294,9 @@ function setDefaults()
function reallySetDefaults()
{
for (let item in g_Controls)
Engine.ConfigDB_RemoveValue("user", item);
for (let category in g_Options)
for (let option of g_Options[category].options)
Engine.ConfigDB_RemoveValue("user", option.parameters.config);
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
revertChanges();
@@ -309,29 +305,23 @@ function reallySetDefaults()
function revertChanges()
{
Engine.ConfigDB_Reload("user");
for (let item in g_Controls)
{
let control = g_Controls[item];
if (control.parameters.function)
{
let value = Engine.ConfigDB_GetValue("user", item);
Engine[control.parameters.function](
(control.type == "string" || control.type == "dropdown") ?
value :
control.type == "boolean" ?
value == "true" :
+value);
}
}
Engine.ConfigDB_SetChanges("user", false);
init();
for (let category in g_Options)
for (let option of g_Options[category].options)
if (option.parameters.function)
Engine[option.parameters.function](
g_OptionType[option.type].configToValue(
Engine.ConfigDB_GetValue("user", option.parameters.config)));
displayOptions();
}
function saveChanges()
{
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
Engine.ConfigDB_SetChanges("user", false);
updateOptionPanel();
enableButtons();
}
/**
@@ -1,5 +1,7 @@
{
"generalSetting":
[
{
"label": "General",
"options":
[
{
"type": "string",
@@ -135,8 +137,12 @@
"tooltip": "Display the healing range of selected units (can also be toggled in-game with the hotkey).",
"parameters": { "config": "gui.session.healrange" }
}
],
"graphicsSetting":
]
},
{
"label": "Graphics",
"tooltip": "Set the balance between performance and visual appearance.",
"options":
[
{
"type": "boolean",
@@ -269,8 +275,11 @@
"tooltip": "To save CPU workload, throttle render frequency in running games. Set to maximum to disable throttling.",
"parameters": { "config": "adaptivefps.session", "min": 20, "max": 100 }
}
],
"soundSetting":
]
},
{
"label": "Sound",
"options":
[
{
"type": "slider",
@@ -308,8 +317,12 @@
"tooltip": "Receive audio notification when someone types your nick",
"parameters": { "config": "sound.notify.nick" }
}
],
"lobbySetting":
]
},
{
"label": "Lobby",
"tooltip": "These settings only affect the multiplayer.",
"options":
[
{
"type": "number",
@@ -323,8 +336,12 @@
"tooltip": "Show the average rating of the participating players in a column of the gamelist.",
"parameters": { "config": "lobby.columns.gamerating" }
}
],
"notificationSetting":
]
},
{
"label": "Chat Notifications",
"tooltip": "Regulate the verbosity of chat notifications.",
"options":
[
{
"type": "boolean",
@@ -358,4 +375,5 @@
}
}
]
}
}
]
@@ -1,12 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
==========================================
- Options Window -
==========================================
-->
<objects>
<script file="gui/common/functions_global_object.js"/>
<script file="gui/options/options.js"/>
@@ -14,95 +9,58 @@
<object type="image" sprite="ModernFade"/>
<!-- Settings Window -->
<object name="options" type="image" style="ModernDialog" size="50%-476 50%-316 50%+476 50%+316">
<object name="options" type="image" style="ModernDialog" size="50%-350 50%-316 50%+350 50%+316">
<object style="ModernLabelText" type="text" size="50%-128 -16 50%+128 16">
<translatableAttribute id="caption">Game Options</translatableAttribute>
</object>
<object name="GeneralSettings" type="image" sprite="ModernDarkBoxGold" size="16 16 332 100%-52">
<object style="ModernLabelText" type="text" size="0 5 100% 25">
<translatableAttribute id="caption">General</translatableAttribute>
</object>
<repeat count="23">
<object name="generalSetting[n]" size="0 25 100% 50" hidden="true">
<object name="generalSettingLabel[n]" size="0 0 65% 100%" type="text" style="ModernLeftLabelText"/>
<object name="generalSettingTickbox[n]" size="90% 5 100% 100%+5" type="checkbox" style="ModernTickBox" hidden="true"/>
<object name="generalSettingInput[n]" size="70% 0 100%-8 100%" type="input" style="ModernInput" hidden="true"/>
<object name="generalSettingDropdown[n]" size="70% 0 100%-8 100%" type="dropdown" style="ModernDropDown" hidden="true"/>
<!-- Category Tabs -->
<object name="tabButtons" type="image" sprite="ModernDarkBoxGold" size="15 16 210 100%-52">
<repeat count="15">
<object name="tabButton[n]" type="button" sprite="ModernTabButtonVertical" size="0 0 195 30" hidden="true">
<object type="text" name="tabButtonText[n]" style="ModernLabelText" ghost="true"/>
</object>
</repeat>
</object>
<object name="GraphicsSettings" type="image" sprite="ModernDarkBoxGold" size="340 16 632 100%-52">
<object style="ModernLabelText" type="text" size="0 5 100% 25">
<translatableAttribute id="caption">Graphics Settings</translatableAttribute>
</object>
<repeat count="22">
<object name="graphicsSetting[n]" size="0 25 100% 50" hidden="true">
<object name="graphicsSettingLabel[n]" size="0 0 65% 100%" type="text" style="ModernLeftLabelText"/>
<object name="graphicsSettingTickbox[n]" size="90% 5 100% 100%+5" type="checkbox" style="ModernTickBox" hidden="true"/>
<object name="graphicsSettingSlider[n]" size="70% 0 100%-8 100%" type="slider" style="ModernSlider" hidden="true"/>
<object name="graphicsSettingInput[n]" size="70% 0 100%-8 100%" type="input" style="ModernInput" hidden="true"/>
<object name="graphicsSettingDropdown[n]" size="70% 0 100%-8 100%" type="dropdown" style="ModernDropDown" hidden="true"/>
</object>
</repeat>
</object>
<object name="SoundSettings" type="image" sprite="ModernDarkBoxGold" size="640 16 936 40%">
<object style="ModernLabelText" type="text" size="0 5 100% 25">
<translatableAttribute id="caption">Sound Settings</translatableAttribute>
</object>
<repeat count="10">
<object name="soundSetting[n]" size="0 25 100% 50" hidden="true">
<object name="soundSettingLabel[n]" size="0 0 65% 100%" type="text" style="ModernLeftLabelText"/>
<object name="soundSettingTickbox[n]" size="90% 5 100% 100%+5" type="checkbox" style="ModernTickBox" hidden="true"/>
<object name="soundSettingSlider[n]" size="70% 0 100%-8 100%" type="slider" style="ModernSlider" hidden="true"/>
<object name="soundSettingDropdown[n]" size="70% 0 100%-8 100%" type="dropdown" style="ModernDropDown" hidden="true"/>
</object>
</repeat>
</object>
<object name="LobbySettings" type="image" sprite="ModernDarkBoxGold" size="640 40%+8 936 65%">
<object style="ModernLabelText" type="text" size="0 5 100% 25">
<translatableAttribute id="caption">Lobby Settings</translatableAttribute>
</object>
<repeat count="5">
<object name="lobbySetting[n]" size="0 25 100% 50" hidden="true">
<object name="lobbySettingLabel[n]" size="0 0 65% 100%" type="text" style="ModernLeftLabelText"/>
<object name="lobbySettingTickbox[n]" size="90% 5 100% 100%+5" type="checkbox" style="ModernTickBox" hidden="true"/>
<object name="lobbySettingInput[n]" size="70% 0 100%-8 100%" type="input" style="ModernInput" hidden="true"/>
<object name="lobbySettingDropdown[n]" size="70% 0 100%-8 100%" type="dropdown" style="ModernDropDown" hidden="true"/>
</object>
</repeat>
</object>
<object name="NotificationsSettings" type="image" sprite="ModernDarkBoxGold" size="640 65%+8 936 100%-52">
<object style="ModernLabelText" type="text" size="0 5 100% 25">
<translatableAttribute id="caption">Chat Notification Settings</translatableAttribute>
</object>
<repeat count="10">
<object name="notificationSetting[n]" size="0 25 100% 50" hidden="true">
<object name="notificationSettingLabel[n]" size="0 0 65% 100%" type="text" style="ModernLeftLabelText"/>
<object name="notificationSettingTickbox[n]" size="90% 5 100% 100%+5" type="checkbox" style="ModernTickBox" hidden="true"/>
<object name="notificationSettingInput[n]" size="70% 0 100%-8 100%" type="input" style="ModernInput" hidden="true"/>
<object name="notificationSettingDropdown[n]" size="50% 0 100%-8 100%" type="dropdown" style="ModernDropDown" hidden="true"/>
<!-- Option Controls -->
<object name="option_controls" type="image" sprite="ModernDarkBoxGold" size="220 16 685 100%-52">
<repeat count="25">
<object name="option_control[n]" size="0 0 100% 0">
<object name="option_label[n]" size="0 0 60% 100%" type="text" style="ModernLeftLabelText"/>
<object name="option_control_boolean[n]" size="90% 2 100% 100%+2" type="checkbox" style="ModernTickBox"/>
<object name="option_control_string[n]" size="65% 0 100%-8 100%" type="input" style="ModernInput"/>
<object name="option_control_number[n]" size="65% 0 100%-8 100%" type="input" style="ModernInput"/>
<object name="option_control_dropdown[n]" size="65% 0 100%-8 100%" type="dropdown" style="ModernDropDown"/>
<object name="option_control_slider[n]" size="65% 0 100%-8 100%" type="slider" style="ModernSlider"/>
</object>
</repeat>
</object>
<object type="button" style="ModernButtonRed" size="50%-236 100%-44 50%-136 100%-16">
<translatableAttribute id="caption">Reset</translatableAttribute>
<translatableAttribute id="tooltip">Resets user settings to their game default</translatableAttribute>
<action on="Press">setDefaults();</action>
</object>
<object name="revertChanges" type="button" style="ModernButtonRed" size="50%-104 100%-44 50%-4 100%-16">
<translatableAttribute id="caption">Revert</translatableAttribute>
<translatableAttribute id="tooltip">Reverts to previous saved settings</translatableAttribute>
<action on="Press">revertChanges();</action>
</object>
<object name="saveChanges" type="button" style="ModernButtonRed" size="50%+4 100%-44 50%+104 100%-16">
<translatableAttribute id="caption">Save</translatableAttribute>
<translatableAttribute id="tooltip">Saves changes</translatableAttribute>
<action on="Press">saveChanges();</action>
</object>
<object type="button" style="ModernButtonRed" size="50%+136 100%-44 50%+236 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>
</objects>