Index: ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js (revision 25965)
+++ ps/trunk/binaries/data/mods/mod/gui/common/functions_msgbox.js (revision 25966)
@@ -1,29 +1,47 @@
function messageBox(mbWidth, mbHeight, mbMessage, mbTitle, mbButtonCaptions, mbBtnCode, mbCallbackArgs)
{
Engine.PushGuiPage(
"page_msgbox.xml",
{
"width": mbWidth,
"height": mbHeight,
"message": mbMessage,
"title": mbTitle,
"buttonCaptions": mbButtonCaptions
},
btnCode => {
if (mbBtnCode !== undefined && mbBtnCode[btnCode])
mbBtnCode[btnCode](mbCallbackArgs ? mbCallbackArgs[btnCode] : undefined);
});
}
+function timedConfirmation(width, height, message, timeout, title, buttonCaptions, btnCode, callbackArgs)
+{
+ Engine.PushGuiPage(
+ "page_timedconfirmation.xml",
+ {
+ "width": width,
+ "height": height,
+ "message": message,
+ "timeout": timeout,
+ "title": title,
+ "buttonCaptions": buttonCaptions
+ },
+ button => {
+ if (btnCode !== undefined && btnCode[button])
+ btnCode[button](callbackArgs ? callbackArgs[button] : undefined);
+ });
+}
+
function openURL(url)
{
Engine.OpenURL(url);
messageBox(
600, 200,
sprintf(
translate("Opening %(url)s\n in default web browser. Please wait…"),
{ "url": url }
),
translate("Opening page"));
}
Index: ps/trunk/binaries/data/mods/mod/gui/common/utilities.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/utilities.js (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/common/utilities.js (revision 25966)
@@ -0,0 +1,35 @@
+function distributeButtonsHorizontally(button, captions)
+{
+ const y1 = "100%-46";
+ const y2 = "100%-18";
+ switch (captions.length)
+ {
+ case 1:
+ button[0].size = "18 " + y1 + " 100%-18 " + y2;
+ break;
+ case 2:
+ button[0].size = "18 " + y1 + " 50%-5 " + y2;
+ button[1].size = "50%+5 " + y1 + " 100%-18 " + y2;
+ break;
+ case 3:
+ button[0].size = "18 " + y1 + " 33%-5 " + y2;
+ button[1].size = "33%+5 " + y1 + " 66%-5 " + y2;
+ button[2].size = "66%+5 " + y1 + " 100%-18 " + y2;
+ break;
+ }
+}
+
+function setButtonCaptionsAndVisibitily(button, captions, cancelHotkey, name)
+{
+ captions.forEach((caption, i) => {
+ button[i] = Engine.GetGUIObjectByName(name + (i + 1));
+ button[i].caption = caption;
+ button[i].hidden = false;
+ button[i].onPress = () => {
+ Engine.PopGuiPage(i);
+ };
+
+ if (i == 0)
+ cancelHotkey.onPress = button[i].onPress;
+ });
+}
Index: ps/trunk/binaries/data/mods/mod/gui/msgbox/msgbox.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/msgbox/msgbox.js (revision 25965)
+++ ps/trunk/binaries/data/mods/mod/gui/msgbox/msgbox.js (revision 25966)
@@ -1,60 +1,30 @@
/**
* Currently limited to at most 3 buttons per message box.
* The convention is to have "cancel" appear first.
*/
function init(data)
{
// Set title
Engine.GetGUIObjectByName("mbTitleBar").caption = data.title;
// Set subject
let mbTextObj = Engine.GetGUIObjectByName("mbText");
mbTextObj.caption = data.message;
if (data.font)
mbTextObj.font = data.font;
// Default behaviour
let mbCancelHotkey = Engine.GetGUIObjectByName("mbCancelHotkey");
mbCancelHotkey.onPress = Engine.PopGuiPage;
// Calculate size
let mbLRDiff = data.width / 2;
let mbUDDiff = data.height / 2;
Engine.GetGUIObjectByName("mbMain").size = "50%-" + mbLRDiff + " 50%-" + mbUDDiff + " 50%+" + mbLRDiff + " 50%+" + mbUDDiff;
let captions = data.buttonCaptions || [translate("OK")];
- // Set button captions and visibility
let mbButton = [];
- captions.forEach((caption, i) => {
- mbButton[i] = Engine.GetGUIObjectByName("mbButton" + (i + 1));
- mbButton[i].caption = caption;
- mbButton[i].hidden = false;
- mbButton[i].onPress = () => {
- Engine.PopGuiPage(i);
- };
-
- // Convention: Cancel is the first button
- if (i == 0)
- mbCancelHotkey.onPress = mbButton[i].onPress;
- });
-
- // Distribute buttons horizontally
- let y1 = "100%-46";
- let y2 = "100%-18";
- switch (captions.length)
- {
- case 1:
- mbButton[0].size = "18 " + y1 + " 100%-18 " + y2;
- break;
- case 2:
- mbButton[0].size = "18 " + y1 + " 50%-5 " + y2;
- mbButton[1].size = "50%+5 " + y1 + " 100%-18 " + y2;
- break;
- case 3:
- mbButton[0].size = "18 " + y1 + " 33%-5 " + y2;
- mbButton[1].size = "33%+5 " + y1 + " 66%-5 " + y2;
- mbButton[2].size = "66%+5 " + y1 + " 100%-18 " + y2;
- break;
- }
+ setButtonCaptionsAndVisibitily(mbButton, captions, mbCancelHotkey, "mbButton");
+ distributeButtonsHorizontally(mbButton, captions);
}
Index: ps/trunk/binaries/data/mods/mod/gui/page_timedconfirmation.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/page_timedconfirmation.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/page_timedconfirmation.xml (revision 25966)
@@ -0,0 +1,8 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ timedconfirmation/timedconfirmation.xml
+
Index: ps/trunk/binaries/data/mods/mod/gui/timedconfirmation/timedconfirmation.js
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/timedconfirmation/timedconfirmation.js (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/timedconfirmation/timedconfirmation.js (revision 25966)
@@ -0,0 +1,48 @@
+/**
+ * Currently limited to at most 3 buttons per message box.
+ * The convention is to have "cancel" appear first.
+ */
+function init(data)
+{
+ Engine.GetGUIObjectByName("tmcTitleBar").caption = data.title;
+
+ const textObj = Engine.GetGUIObjectByName("tmcText");
+ textObj.caption = data.message;
+
+ updateDisplayedTimer(data.timeout);
+
+ Engine.GetGUIObjectByName("tmcTimer").caption = data.timeout;
+ if (data.font)
+ textObj.font = data.font;
+
+ const cancelHotkey = Engine.GetGUIObjectByName("tmcCancelHotkey");
+ cancelHotkey.onPress = Engine.PopGuiPage;
+
+ const lRDiff = data.width / 2;
+ const uDDiff = data.height / 2;
+ Engine.GetGUIObjectByName("tmcMain").size = "50%-" + lRDiff + " 50%-" + uDDiff + " 50%+" + lRDiff + " 50%+" + uDDiff;
+
+ const captions = data.buttonCaptions || [translate("OK")];
+
+ // Set button captions and visibility
+ const button = [];
+ setButtonCaptionsAndVisibitily(button, captions, cancelHotkey, "tmcButton");
+ distributeButtonsHorizontally(button, captions);
+}
+
+function onTick()
+{
+ const timerObj = Engine.GetGUIObjectByName("tmcTimer");
+ let time = +timerObj.caption;
+ --time;
+ if (time < 1)
+ Engine.GetGUIObjectByName("tmcButton1").onPress();
+
+ timerObj.caption = time;
+ updateDisplayedTimer(time);
+}
+
+function updateDisplayedTimer(time)
+{
+ Engine.GetGUIObjectByName("tmcTimerDisplay").caption = Math.ceil(time / 100);
+}
Index: ps/trunk/binaries/data/mods/mod/gui/timedconfirmation/timedconfirmation.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/timedconfirmation/timedconfirmation.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/mod/gui/timedconfirmation/timedconfirmation.xml (revision 25966)
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
Index: ps/trunk/binaries/data/mods/public/gui/options/options.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.js (revision 25965)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.js (revision 25966)
@@ -1,388 +1,435 @@
/**
* Translated JSON file contents.
*/
var g_Options;
/**
* Names of config keys that have changed, value returned when closing the page.
*/
var g_ChangedKeys;
/**
* 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;
/**
* Color used to indicate that the string entered by the player isn't a sane color.
*/
var g_InsaneColor = "255 0 255";
/**
* 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 - Displays a visual clue if the entered value is invalid and returns a sane value.
* @property tooltip - appends a custom tooltip to the given option description depending on the current value.
*/
var g_OptionType = {
"boolean":
{
"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"
},
"color":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => control.caption,
"guiSetter": "onTextEdit",
"sanitizeValue": (value, control, option) => {
let color = guiToRgbColor(value);
let sanitized = rgbToGuiColor(color);
if (control)
{
control.sprite = sanitized == value ? "ModernDarkBoxWhite" : "ModernDarkBoxWhiteInvalid";
control.children[1].sprite = sanitized == value ? "color:" + value : "color:" + g_InsaneColor;
}
return sanitized;
},
"tooltip": (value, option) =>
sprintf(translate("Default: %(value)s"), {
"value": Engine.ConfigDB_GetValue("default", option.config)
})
},
"number":
{
"configToValue": value => value,
"valueToGui": (value, control) => {
control.caption = value;
},
"guiToValue": control => control.caption,
"guiSetter": "onTextEdit",
"sanitizeValue": (value, control, option) => {
let sanitized =
Math.min(option.max !== undefined ? option.max : +Infinity,
Math.max(option.min !== undefined ? option.min : -Infinity,
isNaN(+value) ? 0 : value));
if (control)
control.sprite = sanitized == value ? "ModernDarkBoxWhite" : "ModernDarkBoxWhiteInvalid";
return sanitized;
},
"tooltip": (value, option) =>
sprintf(
option.min !== undefined && option.max !== undefined ?
translateWithContext("option number", "Min: %(min)s, Max: %(max)s") :
option.min !== undefined && option.max === undefined ?
translateWithContext("option number", "Min: %(min)s") :
option.min === undefined && option.max !== undefined ?
translateWithContext("option number", "Max: %(max)s") :
"",
{
"min": option.min,
"max": option.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.list.map(e => e.label);
control.list_data = option.list.map(e => e.value);
control.onHoverChange = () => {
let item = option.list[control.hovered];
control.tooltip = item && item.tooltip || option.tooltip;
};
}
},
+ "dropdownNumber":
+ {
+ "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.list.map(e => e.label);
+ control.list_data = option.list.map(e => e.value);
+ control.onHoverChange = () => {
+ const item = option.list[control.hovered];
+ control.tooltip = item && item.tooltip || option.tooltip;
+ };
+ },
+ "timeout": (option, oldValue, hasChanges, newValue) => {
+ if (!option.timeout)
+ return;
+ timedConfirmation(
+ 500, 200,
+ translate("Do you want to keep changes?"),
+ 500,
+ translate("Warning"),
+ [translate("No"), translate("Yes")],
+ [() => {this.revertChange(option, +oldValue, hasChanges);}, null]
+ );
+ }
+ },
"slider":
{
"configToValue": value => +value,
"valueToGui": (value, control) => {
control.value = +value;
},
"guiToValue": control => control.value,
"guiSetter": "onValueChange",
"initGUI": (option, control) => {
control.max_value = option.max;
control.min_value = option.min;
},
"tooltip": (value, option) =>
sprintf(translateWithContext("slider number", "Value: %(val)s (min: %(min)s, max: %(max)s)"), {
"val": value.toFixed(2),
"min": option.min.toFixed(2),
"max": option.max.toFixed(2)
})
}
};
function init(data, hotloadData)
{
g_ChangedKeys = hotloadData ? hotloadData.changedKeys : new Set();
g_TabCategorySelected = hotloadData ? hotloadData.tabCategorySelected : 0;
g_Options = Engine.ReadJSONFile("gui/options/options.json");
translateObjectKeys(g_Options, ["label", "tooltip"]);
deepfreeze(g_Options);
placeTabButtons(
g_Options,
false,
g_TabButtonHeight,
g_TabButtonDist,
selectPanel,
displayOptions);
}
function getHotloadData()
{
return {
"tabCategorySelected": g_TabCategorySelected,
"changedKeys": g_ChangedKeys
};
}
/**
* Sets up labels and controls of all options of the currently selected category.
*/
function displayOptions()
{
// Hide all controls
for (let body of Engine.GetGUIObjectByName("option_controls").children)
{
body.hidden = true;
for (let control of body.children)
control.hidden = true;
}
// Initialize label and control of each option for this category
for (let i = 0; i < g_Options[g_TabCategorySelected].options.length; ++i)
{
// 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;
// Load option data
let option = g_Options[g_TabCategorySelected].options[i];
let optionType = g_OptionType[option.type];
let value = optionType.configToValue(Engine.ConfigDB_GetValue("user", option.config));
// Setup control
let control = Engine.GetGUIObjectByName("option_control_" + option.type + "[" + i + "]");
control.tooltip = option.tooltip + (optionType.tooltip ? "\n" + optionType.tooltip(value, option) : "");
control.hidden = false;
if (optionType.initGUI)
optionType.initGUI(option, control);
control[optionType.guiSetter] = function() {};
optionType.valueToGui(value, control);
if (optionType.sanitizeValue)
optionType.sanitizeValue(value, control, option);
control[optionType.guiSetter] = function() {
let value = optionType.guiToValue(control);
if (optionType.sanitizeValue)
optionType.sanitizeValue(value, control, option);
+ const oldValue = optionType.configToValue(Engine.ConfigDB_GetValue("user", option.config));
+
control.tooltip = option.tooltip + (optionType.tooltip ? "\n" + optionType.tooltip(value, option) : "");
+ const hasChanges = Engine.ConfigDB_HasChanges("user");
Engine.ConfigDB_CreateValue("user", option.config, String(value));
Engine.ConfigDB_SetChanges("user", true);
g_ChangedKeys.add(option.config);
fireConfigChangeHandlers(new Set([option.config]));
+ if (option.timeout)
+ optionType.timeout(option, oldValue, hasChanges, value);
+
if (option.function)
Engine[option.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;
}
enableButtons();
}
/**
* Enable exactly the buttons whose dependencies are met.
*/
function enableButtons()
{
g_Options[g_TabCategorySelected].options.forEach((option, i) => {
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;
Engine.GetGUIObjectByName("saveChanges").enabled = hasChanges;
}
function setDefaults()
{
messageBox(
500, 200,
translate("Resetting the options will erase your saved settings. Do you want to continue?"),
translate("Warning"),
[translate("No"), translate("Yes")],
[null, reallySetDefaults]
);
}
function reallySetDefaults()
{
for (let category in g_Options)
for (let option of g_Options[category].options)
{
Engine.ConfigDB_RemoveValue("user", option.config);
g_ChangedKeys.add(option.config);
}
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
revertChanges();
}
+function revertChange(option, oldValue, hadChanges)
+{
+ if (!hadChanges)
+ Engine.ConfigDB_SetChanges("user", false);
+
+ Engine.ConfigDB_CreateValue("user", option.config, String(oldValue));
+ if (option.function)
+ Engine[option.function](oldValue);
+
+ displayOptions();
+}
+
function revertChanges()
{
Engine.ConfigDB_Reload("user");
Engine.ConfigDB_SetChanges("user", false);
for (let category in g_Options)
for (let option of g_Options[category].options)
if (option.function)
Engine[option.function](
g_OptionType[option.type].configToValue(
Engine.ConfigDB_GetValue("user", option.config)));
displayOptions();
}
function saveChanges()
{
for (let category in g_Options)
for (let i = 0; i < g_Options[category].options.length; ++i)
{
let option = g_Options[category].options[i];
let optionType = g_OptionType[option.type];
if (!optionType.sanitizeValue)
continue;
let value = optionType.configToValue(Engine.ConfigDB_GetValue("user", option.config));
if (value == optionType.sanitizeValue(value, undefined, option))
continue;
selectPanel(category);
messageBox(
500, 200,
translate("Some setting values are invalid! Are you sure you want to save them?"),
translate("Warning"),
[translate("No"), translate("Yes")],
[null, reallySaveChanges]
);
return;
}
reallySaveChanges();
}
function reallySaveChanges()
{
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
Engine.ConfigDB_SetChanges("user", false);
enableButtons();
}
/**
* Close GUI page and inform the parent GUI page which options changed.
**/
function closePage()
{
if (Engine.ConfigDB_HasChanges("user"))
messageBox(
500, 200,
translate("You have unsaved changes, do you want to close this window?"),
translate("Warning"),
[translate("No"), translate("Yes")],
[null, closePageWithoutConfirmation]);
else
closePageWithoutConfirmation();
}
function closePageWithoutConfirmation()
{
Engine.PopGuiPage(g_ChangedKeys);
}
Index: ps/trunk/binaries/data/mods/public/gui/options/options.json
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 25965)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.json (revision 25966)
@@ -1,717 +1,735 @@
[
{
"label": "General",
"options":
[
{
"type": "string",
"label": "Player name (single-player)",
"tooltip": "How you want to be addressed in single-player matches.",
"config": "playername.singleplayer"
},
{
"type": "string",
"label": "Player name (multiplayer)",
"tooltip": "How you want to be addressed in multiplayer matches (except lobby).",
"config": "playername.multiplayer"
},
{
"type": "boolean",
"label": "Background pause",
"tooltip": "Pause single-player games when window loses focus.",
"config": "pauseonfocusloss",
"function": "PauseOnFocusLoss"
},
{
"type": "boolean",
"label": "Enable welcome screen",
"tooltip": "If you disable it, the welcome screen will still appear once, each time a new version is available. You can always launch it from the main menu.",
"config": "gui.splashscreen.enable"
},
{
"type": "boolean",
"label": "FPS overlay",
"tooltip": "Show frames per second in top right corner.",
"config": "overlay.fps"
},
{
"type": "boolean",
"label": "Real time overlay",
"tooltip": "Show current system time in top right corner.",
"config": "overlay.realtime"
},
{
"type": "boolean",
"label": "Game time overlay",
"tooltip": "Show current simulation time in top right corner.",
"config": "gui.session.timeelapsedcounter"
},
{
"type": "boolean",
"label": "Ceasefire time overlay",
"tooltip": "Always show the remaining ceasefire time.",
"config": "gui.session.ceasefirecounter"
},
{
"type": "boolean",
"label": "Chat timestamp",
"tooltip": "Display the time at which a chat message was posted.",
"config": "chat.timestamp"
},
{
"type": "dropdown",
"label": "Naming of entities.",
"tooltip": "How to show entity names.",
"config": "gui.session.howtoshownames",
"list": [
{
"value": 0,
"label": "Specific names first",
"tooltip": "Display specific names before generic names."
},
{
"value": 1,
"label": "Generic names first",
"tooltip": "Display generic names before specific names."
},
{
"value": 2,
"label": "Only specific names",
"tooltip": "Display only specific names for entities."
},
{
"value": 3,
"label": "Only generic names",
"tooltip": "Display only generic names for entities."
}
]
}
]
},
{
"label": "Graphics (general)",
"tooltip": "Set the balance between performance and visual appearance.",
"options":
[
{
"type": "boolean",
"label": "Windowed mode",
"tooltip": "Start 0 A.D. in a window.",
"config": "windowed"
},
{
"type": "boolean",
"label": "Fog",
"tooltip": "Enable fog.",
"config": "fog"
},
{
"type": "boolean",
"label": "Post-processing",
"tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).",
"config": "postproc"
},
{
"type": "boolean",
"label": "Shadows",
"tooltip": "Enable shadows.",
"config": "shadows"
},
{
"type": "boolean",
"label": "Unit silhouettes",
"tooltip": "Show outlines of units behind structures.",
"config": "silhouettes"
},
{
"type": "boolean",
"label": "Particles",
"tooltip": "Enable particles.",
"config": "particles"
},
{
"type": "boolean",
"label": "VSync",
"tooltip": "Run vertical sync to fix screen tearing. REQUIRES GAME RESTART",
"config": "vsync"
},
{
"type": "slider",
"label": "FPS throttling in menus",
"tooltip": "To save CPU workload, throttle render frequency in all menus. Set to maximum to disable throttling.",
"config": "adaptivefps.menu",
"min": 20,
"max": 100
},
{
"type": "slider",
"label": "FPS throttling in games",
"tooltip": "To save CPU workload, throttle render frequency in running games. Set to maximum to disable throttling.",
"config": "adaptivefps.session",
"min": 20,
"max": 100
+ },
+ {
+ "type": "dropdownNumber",
+ "label": "GUI scale",
+ "timeout": 500,
+ "tooltip": "GUI scale",
+ "config": "gui.scale",
+ "function": "SetGUIScale",
+ "list": [
+ { "value": 0.75, "label": "75%" },
+ { "value": 1.00, "label": "100%" },
+ { "value": 1.25, "label": "125%" },
+ { "value": 1.50, "label": "150%" },
+ { "value": 1.75, "label": "175%" },
+ { "value": 2.00, "label": "200%" },
+ { "value": 2.25, "label": "225%" },
+ { "value": 2.50, "label": "250%" }
+ ]
}
]
},
{
"label": "Graphics (advanced)",
"tooltip": "More specific rendering settings.",
"options":
[
{
"type": "boolean",
"label": "Prefer GLSL",
"tooltip": "Use OpenGL 2.0 shaders (recommended).",
"config": "preferglsl"
},
{
"type": "boolean",
"label": "Fog",
"tooltip": "Enable fog.",
"dependencies": ["preferglsl"],
"config": "fog"
},
{
"type": "boolean",
"label": "Post-processing",
"tooltip": "Use screen-space post-processing filters (HDR, Bloom, DOF, etc).",
"config": "postproc"
},
{
"type": "dropdown",
"label": "Antialiasing",
"tooltip": "Reduce aliasing effect on edges.",
"dependencies": ["postproc", "preferglsl"],
"config": "antialiasing",
"list": [
{ "value": "disabled", "label": "Disabled", "tooltip": "Do not use antialiasing." },
{ "value": "fxaa", "label": "FXAA", "tooltip": "Fast, but simple antialiasing." },
{ "value": "msaa2", "label": "MSAA (2×)", "tooltip": "Slow, but high-quality antialiasing, uses two samples per pixel. Supported for GL3.3+." },
{ "value": "msaa4", "label": "MSAA (4×)", "tooltip": "Slow, but high-quality antialiasing, uses four samples per pixel. Supported for GL3.3+." },
{ "value": "msaa8", "label": "MSAA (8×)", "tooltip": "Slow, but high-quality antialiasing, uses eight samples per pixel. Supported for GL3.3+." },
{ "value": "msaa16", "label": "MSAA (16×)", "tooltip": "Slow, but high-quality antialiasing, uses sixteen samples per pixel. Supported for GL3.3+." }
]
},
{
"type": "dropdown",
"label": "Sharpening",
"tooltip": "Reduce blurry effects.",
"dependencies": ["postproc", "preferglsl"],
"config": "sharpening",
"list": [
{ "value": "disabled", "label": "Disabled", "tooltip": "Do not use sharpening." },
{ "value": "cas", "label": "FidelityFX CAS", "tooltip": "Contrast adaptive sharpening, a fast, contrast based sharpening pass." }
]
},
{
"type": "slider",
"label": "Sharpness factor",
"tooltip": "The sharpness of the choosen pass.",
"dependencies": ["postproc", "preferglsl"],
"config": "sharpness",
"min": 0,
"max": 1
},
{
"type": "dropdown",
"label": "Model quality",
"tooltip": "Model quality setting.",
"config": "max_actor_quality",
"list": [
{ "value": 100, "label": "Low", "tooltip": "Simpler models for better performance." },
{ "value": 150, "label": "Medium", "tooltip": "Average quality and average performance." },
{ "value": 200, "label": "High", "tooltip": "High quality models." }
]
},
{
"type": "dropdown",
"label": "Model appearance randomization",
"tooltip": "Randomize the appearance of entities. Disabling gives a small performance improvement.",
"config": "variant_diversity",
"list": [
{ "value": "none", "label": "None", "tooltip": "Entities will all look the same." },
{ "value": "limited", "label": "Limited", "tooltip": "Entities will be less diverse." },
{ "value": "full", "label": "Normal", "tooltip": "Entities appearance is randomized normally." }
]
},
{
"type": "slider",
"label": "Shader effects",
"tooltip": "Number of shader effects. REQUIRES GAME RESTART",
"config": "materialmgr.quality",
"min": 0,
"max": 10
},
{
"type": "boolean",
"label": "Shadows",
"tooltip": "Enable shadows.",
"config": "shadows"
},
{
"type": "dropdown",
"label": "Quality",
"tooltip": "Shadow map resolution. High values can crash the game when using a graphics card with low memory!",
"dependencies": ["shadows"],
"config": "shadowquality",
"list": [
{ "value": -1, "label": "Low" },
{ "value": 0, "label": "Medium" },
{ "value": 1, "label": "High" },
{ "value": 2, "label": "Very High" }
]
},
{
"type": "boolean",
"label": "Filtering",
"tooltip": "Smooth shadows.",
"dependencies": ["shadows"],
"config": "shadowpcf"
},
{
"type": "slider",
"label": "Cutoff distance",
"tooltip": "Hides shadows beyond a certain distance from a camera.",
"dependencies": ["shadows"],
"config": "shadowscutoffdistance",
"min": 100,
"max": 1500
},
{
"type": "boolean",
"label": "Cover whole map",
"tooltip": "When ON shadows cover the whole map and shadows cutoff distance is ignored. Useful for making screenshots of a whole map.",
"dependencies": ["shadows"],
"config": "shadowscovermap"
},
{
"type": "boolean",
"label": "Water effects",
"tooltip": "When OFF, use the lowest settings possible to render water. This makes other settings irrelevant.",
"config": "watereffects"
},
{
"type": "boolean",
"label": "High-quality water effects",
"tooltip": "Use higher-quality effects for water, rendering coastal waves, shore foam, and ships trails.",
"dependencies": ["watereffects"],
"config": "waterfancyeffects"
},
{
"type": "boolean",
"label": "Water reflections",
"tooltip": "Allow water to reflect a mirror image.",
"dependencies": ["watereffects"],
"config": "waterreflection"
},
{
"type": "boolean",
"label": "Water refraction",
"tooltip": "Use a real water refraction map and not transparency.",
"dependencies": ["watereffects"],
"config": "waterrefraction"
},
{
"type": "boolean",
"label": "Real water depth",
"tooltip": "Use actual water depth in rendering calculations.",
"dependencies": ["watereffects", "waterrefraction"],
"config": "waterrealdepth"
}
]
},
{
"label": "Sound",
"options":
[
{
"type": "slider",
"label": "Master volume",
"tooltip": "Master audio gain.",
"config": "sound.mastergain",
"function": "SetMasterGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Music volume",
"tooltip": "In game music gain.",
"config": "sound.musicgain",
"function": "SetMusicGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Ambient volume",
"tooltip": "In game ambient sound gain.",
"config": "sound.ambientgain",
"function": "SetAmbientGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "Action volume",
"tooltip": "In game unit action sound gain.",
"config": "sound.actiongain",
"function": "SetActionGain",
"min": 0,
"max": 2
},
{
"type": "slider",
"label": "UI volume",
"tooltip": "UI sound gain.",
"config": "sound.uigain",
"function": "SetUIGain",
"min": 0,
"max": 2
},
{
"type": "boolean",
"label": "Nick notification",
"tooltip": "Receive audio notification when someone types your nick.",
"config": "sound.notify.nick"
},
{
"type": "boolean",
"label": "New player notification in game setup",
"tooltip": "Receive audio notification when a new client joins the game setup.",
"config": "sound.notify.gamesetup.join"
}
]
},
{
"label": "Game Setup",
"options":
[
{
"type": "boolean",
"label": "Enable game setting tips",
"tooltip": "Show tips when setting up a game.",
"config": "gui.gamesetup.enabletips"
},
{
"type": "boolean",
"label": "Enable settings panel slide",
"tooltip": "Slide the settings panel when opening, closing or resizing.",
"config": "gui.gamesetup.settingsslide"
},
{
"type": "boolean",
"label": "Persist match settings",
"tooltip": "Save and restore match settings for quick reuse when hosting another game.",
"config": "persistmatchsettings"
},
{
"type": "dropdown",
"label": "Default AI difficulty",
"tooltip": "Default difficulty of the AI.",
"config": "gui.gamesetup.aidifficulty",
"list": [
{ "value": 0, "label": "Sandbox" },
{ "value": 1, "label": "Very Easy" },
{ "value": 2, "label": "Easy" },
{ "value": 3, "label": "Medium" },
{ "value": 4, "label": "Hard" },
{ "value": 5, "label": "Very Hard" }
]
},
{
"type": "dropdown",
"label": "Default AI behavior",
"tooltip": "Default behavior of the AI.",
"config": "gui.gamesetup.aibehavior",
"list": [
{ "value": "random", "label": "Random" },
{ "value": "balanced", "label": "Balanced" },
{ "value": "aggressive", "label": "Aggressive" },
{ "value": "defensive", "label": "Defensive" }
]
},
{
"type": "dropdown",
"label": "Assign players",
"tooltip": "Automatically assign joining clients to free player slots during the match setup.",
"config": "gui.gamesetup.assignplayers",
"list": [
{
"value": "everyone",
"label": "Everyone",
"tooltip": "Players joining the match will be assigned if there is a free slot."
},
{
"value": "buddies",
"label": "Buddies",
"tooltip": "Players joining the match will only be assigned if they are a buddy of the host and if there is a free slot."
},
{
"value": "disabled",
"label": "Disabled",
"tooltip": "Players only receive a slot when the host assigns them explicitly."
}
]
}
]
},
{
"label": "Networking / Lobby",
"tooltip": "These settings only affect the multiplayer.",
"options":
[
{
"type": "boolean",
"label": "TLS encryption",
"tooltip": "Protect login and data exchanged with the lobby server using TLS encryption.",
"config": "lobby.tls"
},
{
"type": "number",
"label": "Chat backlog",
"tooltip": "Number of backlogged messages to load when joining the lobby.",
"config": "lobby.history",
"min": "0"
},
{
"type": "boolean",
"label": "Game rating column",
"tooltip": "Show the average rating of the participating players in a column of the gamelist.",
"config": "lobby.columns.gamerating"
},
{
"type": "boolean",
"label": "Network warnings",
"tooltip": "Show which player has a bad connection in multiplayer games.",
"config": "overlay.netwarnings"
},
{
"type": "dropdown",
"label": "Late observer joins",
"tooltip": "Allow everybody or buddies only to join the game as observer after it started.",
"config": "network.lateobservers",
"list": [
{ "value": "everyone", "label": "Everyone" },
{ "value": "buddies", "label": "Buddies" },
{ "value": "disabled", "label": "Disabled" }
]
},
{
"type": "number",
"label": "Observer limit",
"tooltip": "Prevent further observers from joining if the limit is reached.",
"config": "network.observerlimit",
"min": 0,
"max": 32
},
{
"type": "number",
"label": "Max lag for observers",
"tooltip": "When hosting, pause the game if observers are lagging more than this many turns. If set to -1, observers are ignored.",
"config": "network.observermaxlag",
"min": -1,
"max": 10000
},
{
"type": "boolean",
"label": "(Observer) Speed up when lagging.",
"tooltip": "When observing a game, automatically speed up if you start lagging, to catch up with the live match.",
"config": "network.autocatchup"
}
]
},
{
"label": "Game Session",
"tooltip": "Change options regarding the in-game settings.",
"options":
[
{
"type": "slider",
"label": "Wounded unit health",
"tooltip": "The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number.",
"config": "gui.session.woundedunithotkeythreshold",
"min": 0,
"max": 100
},
{
"type": "number",
"label": "Batch training size",
"tooltip": "Number of units trained per batch by default.",
"config": "gui.session.batchtrainingsize",
"min": 1,
"max": 20
},
{
"type": "slider",
"label": "Scroll batch increment ratio",
"tooltip": "Number of times you have to scroll to increase/decrease the batchsize by 1.",
"config": "gui.session.scrollbatchratio",
"min": 0.1,
"max": 30
},
{
"type": "slider",
"label": "Flare display duration",
"tooltip": "How long the flare markers on the minimap are displayed in seconds.",
"config": "gui.session.flarelifetime",
"min": 0,
"max": 60
},
{
"type": "boolean",
"label": "Chat notification attack",
"tooltip": "Show a chat notification if you are attacked by another player.",
"config": "gui.session.notifications.attack"
},
{
"type": "boolean",
"label": "Chat notification tribute",
"tooltip": "Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode.",
"config": "gui.session.notifications.tribute"
},
{
"type": "boolean",
"label": "Chat notification barter",
"tooltip": "Show a chat notification to observers when a player bartered resources.",
"config": "gui.session.notifications.barter"
},
{
"type": "dropdown",
"label": "Chat notification phase",
"tooltip": "Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode.",
"config": "gui.session.notifications.phase",
"list": [
{ "value": "none", "label": "Disable" },
{ "value": "completed", "label": "Completed" },
{ "value": "all", "label": "All displayed" }
]
},
{
"type": "boolean",
"label": "Attack range visualization",
"tooltip": "Display the attack range of selected defensive structures. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.attackrange"
},
{
"type": "boolean",
"label": "Aura range visualization",
"tooltip": "Display the range of auras of selected units and structures. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.aurasrange"
},
{
"type": "boolean",
"label": "Heal range visualization",
"tooltip": "Display the healing range of selected units. (It can also be toggled with the hotkey during a game).",
"config": "gui.session.healrange"
},
{
"type": "boolean",
"label": "Rank icon above status bar",
"tooltip": "Show rank icons above status bars.",
"config": "gui.session.rankabovestatusbar"
},
{
"type": "boolean",
"label": "Experience status bar",
"tooltip": "Show an experience status bar above each selected unit.",
"config": "gui.session.experiencestatusbar"
},
{
"type": "boolean",
"label": "Detailed tooltips",
"tooltip": "Show detailed tooltips for trainable units in unit-producing structures.",
"config": "showdetailedtooltips"
},
{
"type": "dropdown",
"label": "Sort resources and population tooltip",
"tooltip": "Dynamically sort players in the resources and population tooltip by value.",
"config": "gui.session.respoptooltipsort",
"list": [
{ "value": 0, "label": "Unordered" },
{ "value": -1, "label": "Ascending" },
{ "value": 1, "label": "Descending" }
]
},
{
"type": "color",
"label": "Diplomacy colors: self",
"tooltip": "Color of your units when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.self"
},
{
"type": "color",
"label": "Diplomacy colors: ally",
"tooltip": "Color of allies when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.ally"
},
{
"type": "color",
"label": "Diplomacy colors: neutral",
"tooltip": "Color of neutral players when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.neutral"
},
{
"type": "color",
"label": "Diplomacy colors: enemy",
"tooltip": "Color of enemies when diplomacy colors are enabled.",
"config": "gui.session.diplomacycolors.enemy"
},
{
"type": "dropdown",
"label": "Snap to edges",
"tooltip": "This option allows to align new structures with nearby structures.",
"config": "gui.session.snaptoedges",
"list": [
{
"value": "disabled",
"label": "Hotkey to enable snapping",
"tooltip": "New structures are aligned with nearby structures while pressing the hotkey."
},
{
"value": "enabled",
"label": "Hotkey to disable snapping",
"tooltip": "New structures are aligned with nearby structures unless the hotkey is pressed."
}
]
},
{
"type": "dropdown",
"label": "Control group membership",
"tooltip": "Decide whether units can be part of multiple control groups.",
"config": "gui.session.disjointcontrolgroups",
"list": [
{
"value": "true",
"label": "Single",
"tooltip": "When adding a Unit or Structure to a control group, they are removed from other control groups. Use this choice if you want control groups to refer to distinct armies."
},
{
"value": "false",
"label": "Multiple",
"tooltip": "Units and Structures can be part of multiple control groups. This is useful to keep control groups for distinct armies and a control group for the entire army simultaneously."
}
]
},
{
"type": "dropdown",
"label": "Formation control",
"tooltip": "Decide whether formations are enabled for all orders or only 'Walk' and 'Patrol'.",
"config": "gui.session.formationwalkonly",
"list": [
{
"value": "true",
"label": "Walk/Patrol Only",
"tooltip": "Other orders will disband existing formations."
},
{
"value": "false",
"label": "No override",
"tooltip": "Units in formations stay in formations."
}
]
}
]
}
]
Index: ps/trunk/binaries/data/mods/public/gui/options/options.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/options/options.xml (revision 25965)
+++ ps/trunk/binaries/data/mods/public/gui/options/options.xml (revision 25966)
@@ -1,66 +1,67 @@
Game Options
+
Reset
Resets user settings to their game default
setDefaults();
Revert
Reverts to previous saved settings
revertChanges();
Save
Saves changes
saveChanges();
Close
Unsaved changes affect this session only
closePage();
Index: ps/trunk/source/gui/CGUI.cpp
===================================================================
--- ps/trunk/source/gui/CGUI.cpp (revision 25965)
+++ ps/trunk/source/gui/CGUI.cpp (revision 25966)
@@ -1,1315 +1,1316 @@
/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "CGUI.h"
#include "graphics/Canvas2D.h"
#include "gui/IGUIScrollBar.h"
#include "gui/ObjectBases/IGUIObject.h"
#include "gui/ObjectTypes/CGUIDummyObject.h"
#include "gui/ObjectTypes/CTooltip.h"
#include "gui/Scripting/ScriptFunctions.h"
#include "gui/Scripting/JSInterface_GUIProxy.h"
#include "i18n/L10n.h"
#include "lib/allocators/DynamicArena.h"
#include "lib/allocators/STLAllocators.h"
#include "lib/bits.h"
#include "lib/input.h"
#include "lib/sysdep/sysdep.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "maths/Size2D.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/GameSetup/Config.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
+#include "ps/VideoMode.h"
#include "ps/XML/Xeromyces.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
#include
const double SELECT_DBLCLICK_RATE = 0.5;
const u32 MAX_OBJECT_DEPTH = 100; // Max number of nesting for GUI includes. Used to detect recursive inclusion
const CStr CGUI::EventNameLoad = "Load";
const CStr CGUI::EventNameTick = "Tick";
const CStr CGUI::EventNamePress = "Press";
const CStr CGUI::EventNameKeyDown = "KeyDown";
const CStr CGUI::EventNameRelease = "Release";
const CStr CGUI::EventNameMouseRightPress = "MouseRightPress";
const CStr CGUI::EventNameMouseLeftPress = "MouseLeftPress";
const CStr CGUI::EventNameMouseWheelDown = "MouseWheelDown";
const CStr CGUI::EventNameMouseWheelUp = "MouseWheelUp";
const CStr CGUI::EventNameMouseLeftDoubleClick = "MouseLeftDoubleClick";
const CStr CGUI::EventNameMouseLeftRelease = "MouseLeftRelease";
const CStr CGUI::EventNameMouseRightDoubleClick = "MouseRightDoubleClick";
const CStr CGUI::EventNameMouseRightRelease = "MouseRightRelease";
namespace
{
struct VisibleObject
{
IGUIObject* object;
// Index of the object in a depth-first search inside GUI tree.
u32 index;
// Cached value of GetBufferedZ to avoid recursive calls in a deep hierarchy.
float bufferedZ;
};
template
void CollectVisibleObjectsRecursively(const std::vector& objects, Container* visibleObjects)
{
for (IGUIObject* const& object : objects)
{
if (!object->IsHidden())
{
visibleObjects->emplace_back(VisibleObject{object, static_cast(visibleObjects->size()), 0.0f});
CollectVisibleObjectsRecursively(object->GetChildren(), visibleObjects);
}
}
}
} // anonynous namespace
CGUI::CGUI(const std::shared_ptr& context)
: m_BaseObject(std::make_unique(*this)),
m_FocusedObject(nullptr),
m_InternalNameNumber(0),
m_MouseButtons(0)
{
m_ScriptInterface = std::make_shared("Engine", "GUIPage", context);
m_ScriptInterface->SetCallbackData(this);
GuiScriptingInit(*m_ScriptInterface);
m_ScriptInterface->LoadGlobalScripts();
}
CGUI::~CGUI()
{
for (const std::pair& p : m_pAllObjects)
delete p.second;
}
InReaction CGUI::HandleEvent(const SDL_Event_* ev)
{
InReaction ret = IN_PASS;
if (ev->ev.type == SDL_HOTKEYDOWN || ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYUP)
{
const char* hotkey = static_cast(ev->ev.user.data1);
const CStr& eventName = ev->ev.type == SDL_HOTKEYPRESS ? EventNamePress : ev->ev.type == SDL_HOTKEYDOWN ? EventNameKeyDown : EventNameRelease;
if (m_GlobalHotkeys.find(hotkey) != m_GlobalHotkeys.end() && m_GlobalHotkeys[hotkey].find(eventName) != m_GlobalHotkeys[hotkey].end())
{
ret = IN_HANDLED;
ScriptRequest rq(m_ScriptInterface);
JS::RootedObject globalObj(rq.cx, rq.glob);
JS::RootedValue result(rq.cx);
if (!JS_CallFunctionValue(rq.cx, globalObj, m_GlobalHotkeys[hotkey][eventName], JS::HandleValueArray::empty(), &result))
ScriptException::CatchPending(rq);
}
std::map >::iterator it = m_HotkeyObjects.find(hotkey);
if (it != m_HotkeyObjects.end())
for (IGUIObject* const& obj : it->second)
{
if (!obj->IsEnabled())
continue;
if (ev->ev.type == SDL_HOTKEYPRESS)
ret = obj->SendEvent(GUIM_PRESSED, EventNamePress);
else if (ev->ev.type == SDL_HOTKEYDOWN)
ret = obj->SendEvent(GUIM_KEYDOWN, EventNameKeyDown);
else
ret = obj->SendEvent(GUIM_RELEASED, EventNameRelease);
}
}
else if (ev->ev.type == SDL_MOUSEMOTION)
{
// Yes the mouse position is stored as float to avoid
// constant conversions when operating in a
// float-based environment.
- m_MousePos = CVector2D((float)ev->ev.motion.x / g_GuiScale, (float)ev->ev.motion.y / g_GuiScale);
+ m_MousePos = CVector2D((float)ev->ev.motion.x / g_VideoMode.GetScale(), (float)ev->ev.motion.y / g_VideoMode.GetScale());
SGUIMessage msg(GUIM_MOUSE_MOTION);
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::HandleMessage, msg);
}
// Update m_MouseButtons. (BUTTONUP is handled later.)
else if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
case SDL_BUTTON_RIGHT:
case SDL_BUTTON_MIDDLE:
m_MouseButtons |= Bit(ev->ev.button.button);
break;
default:
break;
}
}
// Update m_MousePos (for delayed mouse button events)
CVector2D oldMousePos = m_MousePos;
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
{
- m_MousePos = CVector2D((float)ev->ev.button.x / g_GuiScale, (float)ev->ev.button.y / g_GuiScale);
+ m_MousePos = CVector2D((float)ev->ev.button.x / g_VideoMode.GetScale(), (float)ev->ev.button.y / g_VideoMode.GetScale());
}
// Allow the focused object to pre-empt regular GUI events.
if (GetFocusedObject())
ret = GetFocusedObject()->PreemptEvent(ev);
// Only one object can be hovered
// pNearest will after this point at the hovered object, possibly nullptr
IGUIObject* pNearest = FindObjectUnderMouse();
if (ret == IN_PASS)
{
// Now we'll call UpdateMouseOver on *all* objects,
// we'll input the one hovered, and they will each
// update their own data and send messages accordingly
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast(pNearest));
if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
// Focus the clicked object (or focus none if nothing clicked on)
SetFocusedObject(pNearest);
if (pNearest)
ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_LEFT, EventNameMouseLeftPress);
break;
case SDL_BUTTON_RIGHT:
if (pNearest)
ret = pNearest->SendMouseEvent(GUIM_MOUSE_PRESS_RIGHT, EventNameMouseRightPress);
break;
default:
break;
}
}
else if (ev->ev.type == SDL_MOUSEWHEEL && pNearest)
{
if (ev->ev.wheel.y < 0)
ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_DOWN, EventNameMouseWheelDown);
else if (ev->ev.wheel.y > 0)
ret = pNearest->SendMouseEvent(GUIM_MOUSE_WHEEL_UP, EventNameMouseWheelUp);
}
else if (ev->ev.type == SDL_MOUSEBUTTONUP)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
if (pNearest)
{
double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_LEFT];
pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = timer_Time();
if (timeElapsed < SELECT_DBLCLICK_RATE)
ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_LEFT, EventNameMouseLeftDoubleClick);
else
ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_LEFT, EventNameMouseLeftRelease);
}
break;
case SDL_BUTTON_RIGHT:
if (pNearest)
{
double timeElapsed = timer_Time() - pNearest->m_LastClickTime[SDL_BUTTON_RIGHT];
pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = timer_Time();
if (timeElapsed < SELECT_DBLCLICK_RATE)
ret = pNearest->SendMouseEvent(GUIM_MOUSE_DBLCLICK_RIGHT, EventNameMouseRightDoubleClick);
else
ret = pNearest->SendMouseEvent(GUIM_MOUSE_RELEASE_RIGHT, EventNameMouseRightRelease);
}
break;
}
// Reset all states on all visible objects
m_BaseObject->RecurseObject(&IGUIObject::IsHidden, &IGUIObject::ResetStates);
// Since the hover state will have been reset, we reload it.
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast(pNearest));
}
}
// BUTTONUP's effect on m_MouseButtons is handled after
// everything else, so that e.g. 'press' handlers (activated
// on button up) see which mouse button had been pressed.
if (ev->ev.type == SDL_MOUSEBUTTONUP)
{
switch (ev->ev.button.button)
{
case SDL_BUTTON_LEFT:
case SDL_BUTTON_RIGHT:
case SDL_BUTTON_MIDDLE:
m_MouseButtons &= ~Bit(ev->ev.button.button);
break;
default:
break;
}
}
// Restore m_MousePos (for delayed mouse button events)
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
m_MousePos = oldMousePos;
// Let GUI items handle keys after everything else, e.g. for input boxes.
if (ret == IN_PASS && GetFocusedObject())
{
if (ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_KEYDOWN ||
ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYDOWN ||
ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING)
ret = GetFocusedObject()->ManuallyHandleKeys(ev);
// else will return IN_PASS because we never used the button.
}
return ret;
}
void CGUI::TickObjects()
{
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::Tick);
SendEventToAll(EventNameTick);
m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
}
void CGUI::SendEventToAll(const CStr& eventName)
{
std::unordered_map>::iterator it = m_EventObjects.find(eventName);
if (it == m_EventObjects.end())
return;
std::vector copy = it->second;
for (IGUIObject* object : copy)
object->ScriptEvent(eventName);
}
void CGUI::SendEventToAll(const CStr& eventName, const JS::HandleValueArray& paramData)
{
std::unordered_map>::iterator it = m_EventObjects.find(eventName);
if (it == m_EventObjects.end())
return;
std::vector copy = it->second;
for (IGUIObject* object : copy)
object->ScriptEvent(eventName, paramData);
}
void CGUI::Draw()
{
using Arena = Allocators::DynamicArena<128 * KiB>;
using ObjectListAllocator = ProxyAllocator;
Arena arena;
std::vector visibleObjects((ObjectListAllocator(arena)));
CollectVisibleObjectsRecursively(m_BaseObject->GetChildren(), &visibleObjects);
for (VisibleObject& visibleObject : visibleObjects)
visibleObject.bufferedZ = visibleObject.object->GetBufferedZ();
std::sort(visibleObjects.begin(), visibleObjects.end(), [](const VisibleObject& visibleObject1, const VisibleObject& visibleObject2) -> bool {
if (visibleObject1.bufferedZ != visibleObject2.bufferedZ)
return visibleObject1.bufferedZ < visibleObject2.bufferedZ;
return visibleObject1.index < visibleObject2.index;
});
CCanvas2D canvas;
for (const VisibleObject& visibleObject : visibleObjects)
visibleObject.object->Draw(canvas);
}
void CGUI::DrawSprite(const CGUISpriteInstance& Sprite, CCanvas2D& canvas, const CRect& Rect, const CRect& UNUSED(Clipping))
{
// If the sprite doesn't exist (name == ""), don't bother drawing anything
if (!Sprite)
return;
// TODO: Clipping?
Sprite.Draw(*this, canvas, Rect, m_Sprites);
}
void CGUI::UpdateResolution()
{
m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
}
IGUIObject* CGUI::ConstructObject(const CStr& str)
{
std::map::iterator it = m_ObjectTypes.find(str);
if (it == m_ObjectTypes.end())
return nullptr;
return (*it->second)(*this);
}
bool CGUI::AddObject(IGUIObject& parent, IGUIObject& child)
{
if (child.m_Name.empty())
{
LOGERROR("Can't register an object without name!");
return false;
}
if (m_pAllObjects.find(child.m_Name) != m_pAllObjects.end())
{
LOGERROR("Can't register more than one object of the name %s", child.m_Name.c_str());
return false;
}
m_pAllObjects[child.m_Name] = &child;
parent.RegisterChild(&child);
return true;
}
IGUIObject* CGUI::GetBaseObject()
{
return m_BaseObject.get();
};
bool CGUI::ObjectExists(const CStr& Name) const
{
return m_pAllObjects.find(Name) != m_pAllObjects.end();
}
IGUIObject* CGUI::FindObjectByName(const CStr& Name) const
{
map_pObjects::const_iterator it = m_pAllObjects.find(Name);
if (it == m_pAllObjects.end())
return nullptr;
return it->second;
}
IGUIObject* CGUI::FindObjectUnderMouse()
{
IGUIObject* pNearest = nullptr;
m_BaseObject->RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::ChooseMouseOverAndClosest, pNearest);
return pNearest;
}
CSize2D CGUI::GetWindowSize() const
{
- return CSize2D{static_cast(g_xres) / g_GuiScale, static_cast(g_yres) / g_GuiScale};
+ return CSize2D{static_cast(g_xres) / g_VideoMode.GetScale(), static_cast(g_yres) / g_VideoMode.GetScale() };
}
void CGUI::SetFocusedObject(IGUIObject* pObject)
{
if (pObject == m_FocusedObject)
return;
if (m_FocusedObject)
{
SGUIMessage msg(GUIM_LOST_FOCUS);
m_FocusedObject->HandleMessage(msg);
}
m_FocusedObject = pObject;
if (m_FocusedObject)
{
SGUIMessage msg(GUIM_GOT_FOCUS);
m_FocusedObject->HandleMessage(msg);
}
}
void CGUI::SetObjectStyle(IGUIObject* pObject, const CStr& styleName)
{
// If the style is not recognised (or an empty string) then ApplyStyle will
// emit an error message. Thus we don't need to handle it here.
pObject->ApplyStyle(styleName);
}
void CGUI::UnsetObjectStyle(IGUIObject* pObject)
{
SetObjectStyle(pObject, "default");
}
void CGUI::SetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag)
{
if (!hotkeyTag.empty())
m_HotkeyObjects[hotkeyTag].push_back(pObject);
}
void CGUI::UnsetObjectHotkey(IGUIObject* pObject, const CStr& hotkeyTag)
{
if (hotkeyTag.empty())
return;
std::vector& assignment = m_HotkeyObjects[hotkeyTag];
assignment.erase(
std::remove_if(
assignment.begin(),
assignment.end(),
[&pObject](const IGUIObject* hotkeyObject)
{ return pObject == hotkeyObject; }),
assignment.end());
}
void CGUI::SetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName, JS::HandleValue function)
{
ScriptRequest rq(*m_ScriptInterface);
if (hotkeyTag.empty())
{
ScriptException::Raise(rq, "Cannot assign a function to an empty hotkey identifier!");
return;
}
// Only support "Press", "Keydown" and "Release" events.
if (eventName != EventNamePress && eventName != EventNameKeyDown && eventName != EventNameRelease)
{
ScriptException::Raise(rq, "Cannot assign a function to an unsupported event!");
return;
}
if (!function.isObject() || !JS_ObjectIsFunction(&function.toObject()))
{
ScriptException::Raise(rq, "Cannot assign non-function value to global hotkey '%s'", hotkeyTag.c_str());
return;
}
UnsetGlobalHotkey(hotkeyTag, eventName);
m_GlobalHotkeys[hotkeyTag][eventName].init(rq.cx, function);
}
void CGUI::UnsetGlobalHotkey(const CStr& hotkeyTag, const CStr& eventName)
{
std::map>::iterator it = m_GlobalHotkeys.find(hotkeyTag);
if (it == m_GlobalHotkeys.end())
return;
m_GlobalHotkeys[hotkeyTag].erase(eventName);
if (m_GlobalHotkeys.count(hotkeyTag) == 0)
m_GlobalHotkeys.erase(it);
}
const SGUIScrollBarStyle* CGUI::GetScrollBarStyle(const CStr& style) const
{
std::map::const_iterator it = m_ScrollBarStyles.find(style);
if (it == m_ScrollBarStyles.end())
return nullptr;
return &it->second;
}
/**
* @callgraph
*/
void CGUI::LoadXmlFile(const VfsPath& Filename, std::unordered_set& Paths)
{
Paths.insert(Filename);
CXeromyces xeroFile;
if (xeroFile.Load(g_VFS, Filename, "gui") != PSRETURN_OK)
return;
XMBElement node = xeroFile.GetRoot();
std::string_view root_name(xeroFile.GetElementStringView(node.GetNodeName()));
if (root_name == "objects")
Xeromyces_ReadRootObjects(xeroFile, node, Paths);
else if (root_name == "sprites")
Xeromyces_ReadRootSprites(xeroFile, node);
else if (root_name == "styles")
Xeromyces_ReadRootStyles(xeroFile, node);
else if (root_name == "setup")
Xeromyces_ReadRootSetup(xeroFile, node);
else
LOGERROR("CGUI::LoadXmlFile encountered an unknown XML root node type: %s", root_name.data());
}
void CGUI::LoadedXmlFiles()
{
m_BaseObject->RecurseObject(nullptr, &IGUIObject::UpdateCachedSize);
SGUIMessage msg(GUIM_LOAD);
m_BaseObject->RecurseObject(nullptr, &IGUIObject::HandleMessage, msg);
SendEventToAll(EventNameLoad);
}
//===================================================================
// XML Reading Xeromyces Specific Sub-Routines
//===================================================================
void CGUI::Xeromyces_ReadRootObjects(const XMBData& xmb, XMBElement element, std::unordered_set& Paths)
{
int el_script = xmb.GetElementID("script");
std::vector > subst;
// Iterate main children
// they should all be or