From c66bd9b1aaac0bcedac1dcee7788e3f54d65a6d2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 May 2015 11:58:55 -0700 Subject: [PATCH] complete addition of paths to DS settings --- .../resources/describe-settings.json | 13 +- domain-server/resources/web/js/settings.js | 308 +++++++++--------- .../{stats => }/js/underscore-keypath.min.js | 0 .../resources/web/settings/index.shtml | 27 +- domain-server/resources/web/stats/index.shtml | 2 +- libraries/shared/src/HifiConfigVariantMap.cpp | 38 +-- libraries/shared/src/HifiConfigVariantMap.h | 12 +- 7 files changed, 197 insertions(+), 203 deletions(-) rename domain-server/resources/web/{stats => }/js/underscore-keypath.min.js (100%) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index d95e77bf11..db7a82c0d0 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -50,12 +50,12 @@ { "name": "paths", "label": "", - "help": "", + "help": "", "type": "table", "key": { "name": "path", "label": "Path", - "placeholder": "/garden" + "placeholder": "/" }, "columns": [ { @@ -63,13 +63,8 @@ "label": "Viewpoint", "placeholder": "/512,512,512" } - ], - "default": { - "/": { - "viewpoint": "/512,512,512" - } - } - } + ] + } ] }, { diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 2313cbe6b1..f956d4a374 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -21,38 +21,34 @@ var Settings = { }; var viewHelpers = { - getFormGroup: function(groupName, setting, values, isAdvanced, isLocked) { - if (groupName) { - setting_name = groupName + "." + setting.name; - } else { - setting_name = setting.name; - } - + getFormGroup: function(keypath, setting, values, isAdvanced, isLocked) { form_group = "
"; - - if (_.has(values, groupName) && _.has(values[groupName], setting.name)) { - setting_value = values[groupName][setting.name]; - } else if (_.has(setting, 'default')) { - setting_value = setting.default; - } else { - setting_value = ""; + + setting_value = _(values).valueForKeyPath(keypath); + + if (!setting_value) { + if (_.has(setting, 'default')) { + setting_value = setting.default; + } else { + setting_value = ""; + } } - + label_class = 'control-label'; if (isLocked) { label_class += ' locked'; } - + common_attrs = " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') - + " " + Settings.TRIGGER_CHANGE_CLASS + "' data-short-name='" + setting.name + "' name='" + setting_name + "' " - + "id='" + setting_name + "'"; - + + " " + Settings.TRIGGER_CHANGE_CLASS + "' data-short-name='" + setting.name + "' name='" + keypath + "' " + + "id='" + keypath + "'"; + if (setting.type === 'checkbox') { if (setting.label) { form_group += "" } form_group += "
" - form_group += "
" @@ -60,52 +56,52 @@ var viewHelpers = { input_type = _.has(setting, 'type') ? setting.type : "text" if (setting.label) { - form_group += ""; + form_group += ""; } - + if (input_type === 'table') { - form_group += makeTable(setting, setting_name, setting_value, isLocked) + form_group += makeTable(setting, keypath, setting_value, isLocked) } else { if (input_type === 'select') { - form_group += "'" + _.each(setting.options, function(option) { - form_group += "" }) - + form_group += "" - + form_group += "" } else { - + if (input_type == 'integer') { input_type = "text" } - + form_group += "" } - + form_group += "" + setting.help + "" - } + } } - + form_group += "
" return form_group } } -$(document).ready(function(){ +$(document).ready(function(){ /* - * Clamped-width. + * Clamped-width. * Usage: *
This long content will force clamped width
* * Author: LV */ - + $('[data-clampedwidth]').each(function () { var elem = $(this); var parentPanel = elem.data('clampedwidth'); @@ -117,52 +113,52 @@ $(document).ready(function(){ resizeFn(); $(window).resize(resizeFn); }) - + $('#settings-form').on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ addTableRow(this); }) - + $('#settings-form').on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ deleteTableRow(this); }) - + $('#settings-form').on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){ moveTableRow(this, true); }) - + $('#settings-form').on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){ moveTableRow(this, false); }) - + $('#settings-form').on('keypress', 'table input', function(e){ if (e.keyCode == 13) { // capture enter in table input // if we have a sibling next to us that has an input, jump to it, otherwise check if we have a glyphicon for add to click sibling = $(this).parent('td').next(); - + if (sibling.hasClass(Settings.DATA_COL_CLASS)) { // set focus to next input sibling.find('input').focus() } else if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click() - + // set focus to the first input in the new row $(this).closest('table').find('tr.inputs input:first').focus() - } + } } }); - + $('#settings-form').on('change', '.' + Settings.TRIGGER_CHANGE_CLASS , function(){ // this input was changed, add the changed data attribute to it $(this).attr('data-changed', true) - + badgeSidebarForDifferences($(this)) }) - + $('#advanced-toggle-button').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) - + if (Settings.showAdvanced) { advancedSelector.show() $(this).html("Hide advanced") @@ -170,48 +166,48 @@ $(document).ready(function(){ advancedSelector.hide() $(this).html("Show advanced") } - + $(this).blur() }) - + $('#settings-form').on('click', '#choose-domain-btn', function(){ chooseFromHighFidelityDomains($(this)) }) - + $('#settings-form').on('change', 'select', function(){ $("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change() }) var panelsSource = $('#panels-template').html() Settings.panelsTemplate = _.template(panelsSource) - + var sidebarTemplate = $('#list-group-template').html() Settings.sidebarTemplate = _.template(sidebarTemplate) - + // $('body').scrollspy({ target: '#setup-sidebar'}) - + reloadSettings(); }) function reloadSettings() { $.getJSON('/settings.json', function(data){ _.extend(data, viewHelpers) - + $('.nav-stacked').html(Settings.sidebarTemplate(data)) $('#panels').html(Settings.panelsTemplate(data)) - + Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); - + // add tooltip to locked settings $('label.locked').tooltip({ placement: 'right', title: 'This setting is in the master config file and cannot be changed' }) - + if (!_.has(data["locked"], "metaverse") && !_.has(data["locked"]["metaverse"], "id")) { // append the domain selection modal, as long as it's not locked appendDomainSelectionModal() - } + } }); } @@ -228,20 +224,20 @@ $('body').on('click', '.save-button', function(e){ $("input:not([data-changed])").each(function(){ $(this).prop('disabled', true); }); - + // grab a JSON representation of the form via form2js var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); - + console.log(formJSON); - + // re-enable all inputs $("input").each(function(){ $(this).prop('disabled', false); }); - + // remove focus from the button $(this).blur() - + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved $.ajax('/settings.json', { data: JSON.stringify(formJSON), @@ -258,41 +254,41 @@ $('body').on('click', '.save-button', function(e){ showErrorMessage("Error", SETTINGS_ERROR_MESSAGE) reloadSettings(); }); - + return false; }); -function makeTable(setting, setting_name, setting_value, isLocked) { +function makeTable(setting, keypath, setting_value, isLocked) { var isArray = !_.has(setting, 'key') - + if (!isArray && setting.can_order) { setting.can_order = false; } - + var html = ""; - + if (setting.help) { html += "" + setting.help + "" } - html += "" - + html += "
" + // Column names html += "" - + if (setting.numbered === true) { html += "" // Row number } - + if (setting.key) { html += "" // Key } - + _.each(setting.columns, function(col) { html += "" // Data }) - + if (!isLocked) { if (setting.can_order) { html += "" } - + // populate rows in the table from existing values var row_num = 1 - + _.each(setting_value, function(row, indexOrName) { - html += "" - + html += "" + if (setting.numbered === true) { html += "" } - + if (setting.key) { html += "" } - + _.each(setting.columns, function(col) { html += "" }) - + if (!isLocked) { if (setting.can_order) { html += "" } - + html += "" - + row_num++ }) - + // populate inputs in the table for new values if (!isLocked) { html += makeTableInputs(setting) } html += "
#" + setting.key.label + "" + col.label + "
" + row_num + "" + indexOrName + "" - + if (isArray) { rowIsObject = setting.columns.length > 1 colValue = rowIsObject ? row[col.name] : row html += colValue - + // for arrays we add a hidden input to this td so that values can be posted appropriately - html += "" } else if (row.hasOwnProperty(col.name)) { - html += row[col.name] + html += row[col.name] } - + html += "
" - + return html; } function makeTableInputs(setting) { var html = "" - + if (setting.numbered === true) { html += "" } - + if (setting.key) { html += "\ \ " } - + _.each(setting.columns, function(col) { html += "\ \ " }) - + if (setting.can_order) { html += "" } html += "" html += "" - + return html } function badgeSidebarForDifferences(changedElement) { // figure out which group this input is in var panelParentID = changedElement.closest('.panel').attr('id') - + // if the panel contains non-grouped settings, the initial value is Settings.initialValues var isGrouped = $(panelParentID).hasClass('grouped'); if (isGrouped) { var initialPanelJSON = Settings.initialValues[panelParentID]; - + // get a JSON representation of that section var panelJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID]; } else { @@ -407,32 +403,32 @@ function badgeSidebarForDifferences(changedElement) { } var badgeValue = 0 - + // badge for any settings we have that are not the same or are not present in initialValues for (var setting in panelJSON) { - if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || - (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) + if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") || + (!_.isEqual(panelJSON[setting], initialPanelJSON[setting]) && (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) { badgeValue += 1 } } - + // update the list-group-item badge to have the new value if (badgeValue == 0) { badgeValue = "" } - + $("a[href='#" + panelParentID + "'] .badge").html(badgeValue); } function addTableRow(add_glyphicon) { var row = $(add_glyphicon).closest('tr') - + var table = row.parents('table') var isArray = table.data('setting-type') === 'array' - + var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) - + if (!isArray) { // Check key spaces var key = row.children(".key").children("input").val() @@ -453,7 +449,7 @@ function addTableRow(add_glyphicon) { return } } - + // Check empty fields var empty = false; _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { @@ -462,23 +458,23 @@ function addTableRow(add_glyphicon) { return } }) - + if (empty) { showErrorMessage("Error", "Empty field(s)") return } - + var input_clone = row.clone() - + // Change input row to data row var table = row.parents("table") - var setting_name = table.attr("name") + var setting_name = table.attr("name") var full_name = setting_name + "." + key row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) row.removeClass("inputs") - + _.each(row.children(), function(element) { - if ($(element).hasClass("numbered")) { + if ($(element).hasClass("numbered")) { // Index row var numbers = columns.children(".numbered") if (numbers.length > 0) { @@ -498,88 +494,88 @@ function addTableRow(add_glyphicon) { var input = $(element).children("input") $(element).html(input.val()) input.remove() - } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { + } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { // Hide inputs var input = $(element).children("input") input.attr("type", "hidden") - + if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length var key = $(element).attr('name') - + // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length - input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", full_name + "." + $(element).attr("name")) } - + input.attr("data-changed", "true") - + $(element).append(input.val()) } else { console.log("Unknown table element") } }) - + input_clone.find('input').each(function(){ $(this).val($(this).attr('data-default')); }); - + if (isArray) { updateDataChangedForSiblingRows(row, true) - + // the addition of any table row should remove the empty-array-row row.siblings('.empty-array-row').remove() } - + badgeSidebarForDifferences($(table)) - + row.parent().append(input_clone) } function deleteTableRow(delete_glyphicon) { var row = $(delete_glyphicon).closest('tr') - + var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' - + row.empty(); - + if (!isArray) { - row.html(""); } else { if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { updateDataChangedForSiblingRows(row) - + // this isn't the last row - we can just remove it row.remove() } else { // this is the last row, we can't remove it completely since we need to post an empty array - + row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS) row.addClass('empty-array-row') - - row.html(""); } } - + // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated badgeSidebarForDifferences($(table)) } function moveTableRow(move_glyphicon, move_up) { var row = $(move_glyphicon).closest('tr') - + var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' if (!isArray) { return; } - + if (move_up) { var prev_row = row.prev() if (prev_row.hasClass(Settings.DATA_ROW_CLASS)) { @@ -591,7 +587,7 @@ function moveTableRow(move_glyphicon, move_up) { next_row.after(row) } } - + // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated badgeSidebarForDifferences($(table)) } @@ -599,23 +595,23 @@ function moveTableRow(move_glyphicon, move_up) { function updateDataChangedForSiblingRows(row, forceTrue) { // anytime a new row is added to an array we need to set data-changed for all sibling row inputs to true // unless it matches the inital set of values - + if (!forceTrue) { // figure out which group this row is in var panelParentID = row.closest('.panel').attr('id') // get the short name for the setting from the table var tableShortName = row.closest('table').data('short-name') - + // get a JSON representation of that section var panelSettingJSON = form2js(panelParentID, ".", false, cleanupFormValues, true)[panelParentID][tableShortName] var initialPanelSettingJSON = Settings.initialValues[panelParentID][tableShortName] - + // if they are equal, we don't need data-changed isTrue = !_.isEqual(panelSettingJSON, initialPanelSettingJSON) } else { isTrue = true } - + row.siblings('.' + Settings.DATA_ROW_CLASS).each(function(){ var hiddenInput = $(this).find('td.' + Settings.DATA_COL_CLASS + ' input') if (isTrue) { @@ -631,19 +627,19 @@ function showRestartModal() { backdrop: 'static', keyboard: false }); - + var secondsElapsed = 0; var numberOfSecondsToWait = 3; - + var refreshSpan = $('span#refresh-time') refreshSpan.html(numberOfSecondsToWait + " seconds"); - + // call ourselves every 1 second to countdown var refreshCountdown = setInterval(function(){ secondsElapsed++; secondsLeft = numberOfSecondsToWait - secondsElapsed refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds")) - + if (secondsElapsed == numberOfSecondsToWait) { location.reload(true); clearInterval(refreshCountdown); @@ -666,22 +662,22 @@ function showErrorMessage(title, message) { function chooseFromHighFidelityDomains(clickedButton) { // setup the modal to help user pick their domain if (Settings.initialValues.metaverse.access_token) { - + // add a spinner to the choose button clickedButton.html("Loading domains...") clickedButton.attr('disabled', 'disabled') - + // get a list of user domains from data-web data_web_domains_url = "https://metaverse.highfidelity.com/api/v1/domains?access_token=" $.getJSON(data_web_domains_url + Settings.initialValues.metaverse.access_token, function(data){ - + modal_buttons = { cancel: { label: 'Cancel', className: 'btn-default' } } - + if (data.data.domains.length) { // setup a select box for the returned domains modal_body = "

Choose the High Fidelity domain you want this domain-server to represent.
This will set your domain ID on the settings page.

" @@ -705,25 +701,25 @@ function chooseFromHighFidelityDomains(clickedButton) { window.open("https://metaverse.highfidelity.com/user/domains", '_blank'); } } - modal_body = "

You do not have any domains in your High Fidelity account." + + modal_body = "

You do not have any domains in your High Fidelity account." + "

Go to your domains page to create a new one. Once your domain is created re-open this dialog to select it.

" } - - + + bootbox.dialog({ title: "Choose matching domain", message: modal_body, buttons: modal_buttons }) - + // remove the spinner from the choose button clickedButton.html("Choose from my domains") clickedButton.removeAttr('disabled') }) - + } else { bootbox.alert({ - message: "You must have an access token to query your High Fidelity domains.

" + + message: "You must have an access token to query your High Fidelity domains.

" + "Please follow the instructions on the settings page to add an access token.", title: "Access token required" }) diff --git a/domain-server/resources/web/stats/js/underscore-keypath.min.js b/domain-server/resources/web/js/underscore-keypath.min.js similarity index 100% rename from domain-server/resources/web/stats/js/underscore-keypath.min.js rename to domain-server/resources/web/js/underscore-keypath.min.js diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index ad6ed0f41c..9bbf913b1a 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -5,7 +5,7 @@
- +
- +
- + - +
- +
- + @@ -90,6 +92,7 @@ + diff --git a/domain-server/resources/web/stats/index.shtml b/domain-server/resources/web/stats/index.shtml index dc7fe9678f..13967d4e36 100644 --- a/domain-server/resources/web/stats/index.shtml +++ b/domain-server/resources/web/stats/index.shtml @@ -9,6 +9,6 @@ - + diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index c92260210e..3fe5d9ec3f 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -72,7 +72,7 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL int configIndex = argumentList.indexOf(CONFIG_FILE_OPTION); QString configFilePath; - + if (configIndex != -1) { // we have a config file - try and read it configFilePath = argumentList[configIndex + 1]; @@ -82,8 +82,8 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL QCoreApplication::organizationName(), QCoreApplication::applicationName()); } - - + + return mergedMap; } @@ -94,23 +94,23 @@ HifiConfigVariantMap::HifiConfigVariantMap() : _userConfig(), _mergedConfig() { - + } void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentList) { // check if there is a master config file const QString MASTER_CONFIG_FILE_OPTION = "--master-config"; - + int masterConfigIndex = argumentList.indexOf(MASTER_CONFIG_FILE_OPTION); if (masterConfigIndex != -1) { QString masterConfigFilepath = argumentList[masterConfigIndex + 1]; - + loadMapFromJSONFile(_masterConfig, masterConfigFilepath); } - + // load the user config const QString USER_CONFIG_FILE_OPTION = "--user-config"; - + int userConfigIndex = argumentList.indexOf(USER_CONFIG_FILE_OPTION); if (userConfigIndex != -1) { _userConfigFilename = argumentList[userConfigIndex + 1]; @@ -119,26 +119,26 @@ void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentLi QCoreApplication::organizationName(), QCoreApplication::applicationName()); } - + loadMapFromJSONFile(_userConfig, _userConfigFilename); - + // the merged config is initially matched to the master config _mergedConfig = _masterConfig; - + // then we merge in anything missing from the user config addMissingValuesToExistingMap(_mergedConfig, _userConfig); } void HifiConfigVariantMap::loadMapFromJSONFile(QVariantMap& existingMap, const QString& filename) { QFile configFile(filename); - + if (configFile.exists()) { qCDebug(shared) << "Reading JSON config file at" << filename; configFile.open(QIODevice::ReadOnly); - + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); existingMap = configDocument.toVariant().toMap(); - + } else { qCDebug(shared) << "Could not find JSON config file at" << filename; } @@ -148,7 +148,7 @@ void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMa foreach(const QString& key, newMap.keys()) { if (existingMap.contains(key)) { // if this is just a regular value, we're done - we don't ovveride - + if (newMap[key].canConvert(QMetaType::QVariantMap) && existingMap[key].canConvert(QMetaType::QVariantMap)) { // there's a variant map below and the existing map has one too, so we need to keep recursing addMissingValuesToExistingMap(*static_cast(existingMap[key].data()), newMap[key].toMap()); @@ -159,11 +159,11 @@ void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMa } } -const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) { +QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) { int dotIndex = keyPath.indexOf('.'); - + QString firstKey = (dotIndex == -1) ? keyPath : keyPath.mid(0, dotIndex); - + if (variantMap.contains(firstKey)) { if (dotIndex == -1) { return &variantMap[firstKey]; @@ -171,6 +171,6 @@ const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath) return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1)); } } - + return NULL; } diff --git a/libraries/shared/src/HifiConfigVariantMap.h b/libraries/shared/src/HifiConfigVariantMap.h index 6bdeb15589..3566f446a2 100644 --- a/libraries/shared/src/HifiConfigVariantMap.h +++ b/libraries/shared/src/HifiConfigVariantMap.h @@ -17,26 +17,26 @@ class HifiConfigVariantMap { public: static QVariantMap mergeCLParametersWithJSONConfig(const QStringList& argumentList); - + HifiConfigVariantMap(); void loadMasterAndUserConfig(const QStringList& argumentList); - + const QVariantMap& getMasterConfig() const { return _masterConfig; } QVariantMap& getUserConfig() { return _userConfig; } QVariantMap& getMergedConfig() { return _mergedConfig; } - + const QString& getUserConfigFilename() const { return _userConfigFilename; } private: QString _userConfigFilename; - + QVariantMap _masterConfig; QVariantMap _userConfig; QVariantMap _mergedConfig; - + void loadMapFromJSONFile(QVariantMap& existingMap, const QString& filename); void addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap); }; -const QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath); +QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath); #endif // hifi_HifiConfigVariantMap_h