From e1b64275c9ea1a08d48b3a64faff22d019193803 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 14:26:39 -0700 Subject: [PATCH 01/13] fix population of multi-column array from values in settings --- .../resources/describe-settings.json | 41 +++++++++++++++++++ domain-server/resources/web/js/settings.js | 22 +++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 64441446a8..e9df7f264d 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -59,6 +59,20 @@ "type": "password", "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true + }, + { + "name": "allowed_users", + "type": "table", + "label": "Allowed Users", + "help": "A list of usernames for the High Fidelity users you want to allow into your domain. Users not found in this list will not be allowed to connect.", + "numbered": false, + "columns": [ + { + "name": "username", + "label": "Username", + "can_set": true + } + ] } ] }, @@ -99,6 +113,33 @@ } ] }, + { + "name": "attenuation_coefficients", + "type": "table", + "label": "Attenuation Coefficients", + "help": "In this table you can set custom attenuation coefficients between audio zones", + "numbered": false, + "columns": [ + { + "name": "source", + "label": "Source", + "can_set": true, + "placeholder": "Zone_A" + }, + { + "name": "listener", + "label": "Listener", + "can_set": true, + "placeholder": "Zone_B" + }, + { + "name": "coefficient", + "label": "Attenuation coefficient", + "can_set": true, + "placeholder": "0.18" + } + ] + }, { "name": "enable_filter", "type": "checkbox", diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 130000fc9c..ce8352e35c 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -210,7 +210,7 @@ function makeTable(setting, setting_name, setting_value) { var html = "" html += "" + setting.help + "" - html += "" // Column names @@ -248,9 +248,15 @@ function makeTable(setting, setting_name, setting_value) { html += "" html += "" + row_num++ }) @@ -358,11 +365,12 @@ function addTableRow(add_glyphicon) { return } }) + if (empty) { showErrorMessage("Error", "Empty field(s)") return } - + var input_clone = row.clone() // Change input row to data row @@ -384,7 +392,7 @@ function addTableRow(add_glyphicon) { } else if ($(element).hasClass("buttons")) { // Change buttons var span = $(element).children("span") - span.removeClass("glyphicon-ok add-row") + span.removeClass("glyphicon-plus add-row") span.addClass("glyphicon-remove del-row") } else if ($(element).hasClass("key")) { var input = $(element).children("input") @@ -396,7 +404,9 @@ function addTableRow(add_glyphicon) { input.attr("type", "hidden") if (isArray) { - input.attr("name", setting_name) + var row_index = row.siblings('tr.row-data').length + + input.attr("name", setting_name + "[" + row_index + "]") } else { input.attr("name", full_name + "." + $(element).attr("name")) } From 98925e4adbbc44dc1554ed2e5bfbd83dd7946651 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 14:34:05 -0700 Subject: [PATCH 02/13] handle removal of last row and removal of hidden input for empty array --- domain-server/resources/web/js/settings.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index ce8352e35c..385759b79c 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -425,6 +425,9 @@ function addTableRow(add_glyphicon) { 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)) @@ -451,6 +454,9 @@ function deleteTableRow(delete_glyphicon) { // this is the last row, we can't remove it completely since we need to post an empty array row.empty() + row.removeClass('new-row row-data') + row.addClass('empty-array-row') + row.html(""); } From 323eaa0ac9696807973d84345563e56fb66e8403 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 14:36:33 -0700 Subject: [PATCH 03/13] remove an extra space --- domain-server/resources/web/js/settings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 385759b79c..4fa7e2bd08 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -492,7 +492,6 @@ function updateDataChangedForSiblingRows(row, forceTrue) { } else { hiddenInput.removeAttr('data-changed') } - }) } From 2472dc0c113297226308fa6dcf2060e9a76186b6 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 14:42:21 -0700 Subject: [PATCH 04/13] handle transition from input to row for array of objects --- domain-server/resources/web/js/settings.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 4fa7e2bd08..f2326d3f11 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -338,15 +338,15 @@ function addTableRow(add_glyphicon) { if (!isArray) { // Check key spaces - var name = row.children(".key").children("input").val() - if (name.indexOf(' ') !== -1) { + var key = row.children(".key").children("input").val() + if (key.indexOf(' ') !== -1) { showErrorMessage("Error", "Key contains spaces") return } // Check keys with the same name var equals = false; _.each(data.children(".key"), function(element) { - if ($(element).text() === name) { + if ($(element).text() === key) { equals = true return } @@ -376,7 +376,7 @@ function addTableRow(add_glyphicon) { // Change input row to data row var table = row.parents("table") var setting_name = table.attr("name") - var full_name = setting_name + "." + name + var full_name = setting_name + "." + key row.addClass("row-data new-row") row.removeClass("inputs") @@ -405,8 +405,12 @@ function addTableRow(add_glyphicon) { if (isArray) { var row_index = row.siblings('tr.row-data').length + var key = $(element).attr('name') - input.attr("name", setting_name + "[" + row_index + "]") + // 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('td.row-data').length + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", full_name + "." + $(element).attr("name")) } From 9dc0bf52b8c6b866843880a28b5e97174e8dd37e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 14:44:24 -0700 Subject: [PATCH 05/13] constantize the advanced settings class --- domain-server/resources/web/js/settings.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index f2326d3f11..7b666be5ec 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -1,12 +1,13 @@ var Settings = { - showAdvanced: false + showAdvanced: false, + ADVANCED_CLASS: 'advanced-setting' }; var viewHelpers = { getFormGroup: function(groupName, setting, values, isAdvanced, isLocked) { setting_name = groupName + "." + setting.name - form_group = "
" + form_group = "
" if (_.has(values, groupName) && _.has(values[groupName], setting.name)) { setting_value = values[groupName][setting.name] @@ -106,7 +107,7 @@ $(document).ready(function(){ $('#advanced-toggle-button').click(function(){ Settings.showAdvanced = !Settings.showAdvanced - var advancedSelector = $('.advanced-setting') + var advancedSelector = $('.' + Settings.ADVANCED_CLASS) if (Settings.showAdvanced) { advancedSelector.show() From ad48592a6976cd40539d17c5cc7110c5032d13fb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 15:02:14 -0700 Subject: [PATCH 06/13] start to constantize some classes in settings.js --- domain-server/resources/web/js/settings.js | 36 ++++++++++++---------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 7b666be5ec..d3faa94b66 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -1,6 +1,8 @@ var Settings = { showAdvanced: false, - ADVANCED_CLASS: 'advanced-setting' + ADVANCED_CLASS: 'advanced-setting', + DATA_ROW_CLASS: 'value-row', + DATA_COL_CLASS: 'value-col' }; var viewHelpers = { @@ -235,7 +237,7 @@ function makeTable(setting, setting_name, setting_value) { var row_num = 1 _.each(setting_value, function(row, indexOrName) { - html += "
" + html += "" if (setting.numbered === true) { html += "" @@ -246,7 +248,7 @@ function makeTable(setting, setting_name, setting_value) { } _.each(setting.columns, function(col) { - html += "" }) @@ -335,7 +337,7 @@ function addTableRow(add_glyphicon) { var table = row.parents('table') var isArray = table.data('setting-type') === 'array' - var data = row.parent().children(".row-data") + var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) if (!isArray) { // Check key spaces @@ -346,7 +348,7 @@ function addTableRow(add_glyphicon) { } // Check keys with the same name var equals = false; - _.each(data.children(".key"), function(element) { + _.each(columns.children(".key"), function(element) { if ($(element).text() === key) { equals = true return @@ -360,7 +362,7 @@ function addTableRow(add_glyphicon) { // Check empty fields var empty = false; - _.each(row.children(".row-data").children("input"), function(element) { + _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { if ($(element).val().length === 0) { empty = true return @@ -378,13 +380,13 @@ function addTableRow(add_glyphicon) { var table = row.parents("table") var setting_name = table.attr("name") var full_name = setting_name + "." + key - row.addClass("row-data new-row") + row.addClass(Settings.DATA_ROW_CLASS + " new-row") row.removeClass("inputs") _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { // Index row - var numbers = data.children(".numbered") + var numbers = columns.children(".numbered") if (numbers.length > 0) { $(element).html(parseInt(numbers.last().text()) + 1) } else { @@ -399,18 +401,18 @@ function addTableRow(add_glyphicon) { var input = $(element).children("input") $(element).html(input.val()) input.remove() - } else if ($(element).hasClass("row-data")) { + } 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('tr.row-data').length + 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('td.row-data').length + var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", full_name + "." + $(element).attr("name")) @@ -450,7 +452,7 @@ function deleteTableRow(delete_glyphicon) { // this is a hash row, so we empty it but leave the hidden input blank so it is cleared when we save row.empty() row.html(""); - } else if (table.find('tr.row-data').length > 1) { + } else if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { updateDataChangedForSiblingRows(row) // this isn't the last row - we can just remove it @@ -459,7 +461,7 @@ function deleteTableRow(delete_glyphicon) { // this is the last row, we can't remove it completely since we need to post an empty array row.empty() - row.removeClass('new-row row-data') + row.removeClass(Settings.DATA_ROW_CLASS).removeClass('new-row') row.addClass('empty-array-row') row.html(" Date: Thu, 9 Oct 2014 15:10:17 -0700 Subject: [PATCH 07/13] handle the enter button in domain server forms --- domain-server/resources/web/js/settings.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index d3faa94b66..b657fc4a7a 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -2,7 +2,9 @@ var Settings = { showAdvanced: false, ADVANCED_CLASS: 'advanced-setting', DATA_ROW_CLASS: 'value-row', - DATA_COL_CLASS: 'value-col' + DATA_COL_CLASS: 'value-col', + ADD_ROW_BUTTON_CLASS: 'add-row', + TABLE_BUTTONS_CLASS: 'buttons' }; var viewHelpers = { @@ -99,6 +101,22 @@ $(document).ready(function(){ $('#settings-form').on('click', '.del-row', function(){ deleteTableRow(this); }) + + $('#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)) { + // jump to that input + sibling.find('input').focus() + } else if (sibling.hasClass(Settings.TABLE_BUTTONS_CLASS)) { + sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click() + $(this).blur() + } + } + }); $('#settings-form').on('change', 'input.trigger-change', function(){ // this input was changed, add the changed data attribute to it From ee36b6b91ad63df1b09bd7a57b375df71702b8d2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 15:12:42 -0700 Subject: [PATCH 08/13] jump to input for next row after enter on previous row --- domain-server/resources/web/js/settings.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index b657fc4a7a..63ac13ae4a 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -109,11 +109,13 @@ $(document).ready(function(){ sibling = $(this).parent('td').next(); if (sibling.hasClass(Settings.DATA_COL_CLASS)) { - // jump to that input + // set focus to next input sibling.find('input').focus() } else if (sibling.hasClass(Settings.TABLE_BUTTONS_CLASS)) { sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click() - $(this).blur() + + // set focus to the first input in the new row + $(this).closest('table').find('tr.inputs input:first').focus() } } }); From 712a81c95087d6ee3ed8ec337382e323e5e8c713 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 15:17:45 -0700 Subject: [PATCH 09/13] more constantization and removal of extra line --- domain-server/resources/web/js/settings.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 63ac13ae4a..d6eba5bf23 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -4,6 +4,7 @@ var Settings = { DATA_ROW_CLASS: 'value-row', DATA_COL_CLASS: 'value-col', ADD_ROW_BUTTON_CLASS: 'add-row', + ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row', TABLE_BUTTONS_CLASS: 'buttons' }; @@ -94,7 +95,7 @@ $(document).ready(function(){ $(window).resize(resizeFn); }) - $('#settings-form').on('click', '.add-row', function(){ + $('#settings-form').on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ addTableRow(this); }) @@ -278,8 +279,6 @@ function makeTable(setting, setting_name, setting_value) { // 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] } @@ -319,7 +318,7 @@ function makeTableInputs(setting) { " }) - html += "" + html += "" html += "" return html @@ -415,8 +414,8 @@ function addTableRow(add_glyphicon) { } else if ($(element).hasClass("buttons")) { // Change buttons var span = $(element).children("span") - span.removeClass("glyphicon-plus add-row") - span.addClass("glyphicon-remove del-row") + span.removeClass(Settings.ADD_ROW_SPAN_CLASSES) + span.addClass("glyphicon glyphicon-remove del-row") } else if ($(element).hasClass("key")) { var input = $(element).children("input") $(element).html(input.val()) From 4d13718d8722bf69bed8cddd35bbee4223c887d1 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 15:19:56 -0700 Subject: [PATCH 10/13] constantize the trigger change class for input changes --- domain-server/resources/web/js/settings.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index d6eba5bf23..1f86ec728e 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -1,6 +1,7 @@ var Settings = { showAdvanced: false, ADVANCED_CLASS: 'advanced-setting', + TRIGGER_CHANGE_CLASS: 'trigger-change', DATA_ROW_CLASS: 'value-row', DATA_COL_CLASS: 'value-col', ADD_ROW_BUTTON_CLASS: 'add-row', @@ -28,7 +29,7 @@ var viewHelpers = { } common_attrs = " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') - + " trigger-change' data-short-name='" + setting.name + "' name='" + setting_name + "' " + + " " + Settings.TRIGGER_CHANGE_CLASS + "' data-short-name='" + setting.name + "' name='" + setting_name + "' " if (setting.type === 'checkbox') { form_group += "" @@ -121,7 +122,7 @@ $(document).ready(function(){ } }); - $('#settings-form').on('change', 'input.trigger-change', function(){ + $('#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) From e80cf6fc76996e7ac4be6ba7bcbbb8fdb228d5dc Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 15:21:38 -0700 Subject: [PATCH 11/13] highlight new rows by making them green --- domain-server/resources/web/css/style.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 2766da0830..ad889274d4 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -84,3 +84,8 @@ td.buttons .glyphicon { text-align: center; font-size: 12px; } + +tr.new-row { + color: #3c763d; + background-color: #dff0d8; +} From c748ecb96798a1914b4ee5de2bf8e73ede11475e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 9 Oct 2014 15:22:20 -0700 Subject: [PATCH 12/13] constantize the new row class --- domain-server/resources/web/js/settings.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 1f86ec728e..69c52ceb6c 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -6,7 +6,8 @@ var Settings = { DATA_COL_CLASS: 'value-col', ADD_ROW_BUTTON_CLASS: 'add-row', ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row', - TABLE_BUTTONS_CLASS: 'buttons' + TABLE_BUTTONS_CLASS: 'buttons', + NEW_ROW_CLASS: 'new-row' }; var viewHelpers = { @@ -400,7 +401,7 @@ function addTableRow(add_glyphicon) { var table = row.parents("table") var setting_name = table.attr("name") var full_name = setting_name + "." + key - row.addClass(Settings.DATA_ROW_CLASS + " new-row") + row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) row.removeClass("inputs") _.each(row.children(), function(element) { @@ -481,7 +482,7 @@ function deleteTableRow(delete_glyphicon) { // this is the last row, we can't remove it completely since we need to post an empty array row.empty() - row.removeClass(Settings.DATA_ROW_CLASS).removeClass('new-row') + row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS) row.addClass('empty-array-row') row.html(" Date: Thu, 9 Oct 2014 15:24:12 -0700 Subject: [PATCH 13/13] constantize the delete row button classes --- domain-server/resources/web/js/settings.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/domain-server/resources/web/js/settings.js b/domain-server/resources/web/js/settings.js index 69c52ceb6c..923a01a8a9 100644 --- a/domain-server/resources/web/js/settings.js +++ b/domain-server/resources/web/js/settings.js @@ -6,6 +6,8 @@ var Settings = { DATA_COL_CLASS: 'value-col', ADD_ROW_BUTTON_CLASS: 'add-row', ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row', + DEL_ROW_BUTTON_CLASS: 'del-row', + DEL_ROW_SPAN_CLASSES: 'glyphicon glyphicon-remove del-row', TABLE_BUTTONS_CLASS: 'buttons', NEW_ROW_CLASS: 'new-row' }; @@ -101,7 +103,7 @@ $(document).ready(function(){ addTableRow(this); }) - $('#settings-form').on('click', '.del-row', function(){ + $('#settings-form').on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ deleteTableRow(this); }) @@ -288,7 +290,7 @@ function makeTable(setting, setting_name, setting_value) { html += "" }) - html += "" + html += "" html += "" row_num++ @@ -417,7 +419,7 @@ function addTableRow(add_glyphicon) { // Change buttons var span = $(element).children("span") span.removeClass(Settings.ADD_ROW_SPAN_CLASSES) - span.addClass("glyphicon glyphicon-remove del-row") + span.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { var input = $(element).children("input") $(element).html(input.val())
" if (isArray) { - html += row + colIsArray = _.isArray(row) + colValue = colIsArray ? row : row[col.name] + html += colValue + // for arrays we add a hidden input to this td so that values can be posted appropriately - html += "" + html += "" + + } else if (row.hasOwnProperty(col.name)) { html += row[col.name] } @@ -260,6 +266,7 @@ function makeTable(setting, setting_name, setting_value) { html += "
" + row_num + "" + html += "" if (isArray) { colIsArray = _.isArray(row) @@ -292,7 +294,7 @@ function makeTableInputs(setting) { } _.each(setting.columns, function(col) { - html += "\ + html += "\ \