Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* Copyright 2015 The Chromium Authors. All rights reserved. | 1 /* Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 * Use of this source code is governed by a BSD-style license that can be | 2 * Use of this source code is governed by a BSD-style license that can be |
| 3 * found in the LICENSE file. */ | 3 * found in the LICENSE file. */ |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * 'cr-settings-prefs' is an element which serves as a model for | 7 * 'cr-settings-prefs' is an element which serves as a model for |
| 8 * interaction with settings which are stored in Chrome's | 8 * interaction with settings which are stored in Chrome's |
| 9 * Preferences. | 9 * Preferences. |
| 10 * | 10 * |
| 11 * Example: | 11 * Example: |
| 12 * | 12 * |
| 13 * <cr-settings-prefs id="prefs"></cr-settings-prefs> | 13 * <cr-settings-prefs id="prefs"></cr-settings-prefs> |
| 14 * <cr-settings-a11y-page prefs="{{this.$.prefs}}"></cr-settings-a11y-page> | 14 * <cr-settings-a11y-page prefs="{{this.$.prefs}}"></cr-settings-a11y-page> |
| 15 * | 15 * |
| 16 * @group Chrome Settings Elements | 16 * @group Chrome Settings Elements |
| 17 * @element cr-settings-a11y-page | 17 * @element cr-settings-a11y-page |
| 18 */ | 18 */ |
| 19 (function() { | 19 (function() { |
| 20 'use strict'; | 20 'use strict'; |
| 21 | 21 |
| 22 // TODO(michaelpg): Remove once prefs.js is complete and well-tested. Used | |
| 23 // to catch errors like crbug.com/521884. | |
| 24 var verifySetPref = function(result) { | |
| 25 assert(result, 'Failed to set preference.'); | |
| 26 }; | |
| 27 | |
| 28 /** | |
| 29 * @constructor | |
| 30 * @param {!PrefObject} prefObj | |
|
Dan Beam
2015/08/21 17:13:19
PrefObject -> PrefData
michaelpg
2015/08/26 06:02:26
Why? PrefObject is defined in the settings_private
| |
| 31 */ | |
| 32 function Pref(prefObj) { | |
|
Dan Beam
2015/08/21 17:13:18
prefObj -> data
Dan Beam
2015/08/21 17:13:18
this.data_ = data;
| |
| 33 this.key = prefObj.key; | |
| 34 this.update(prefObj); | |
|
Dan Beam
2015/08/21 17:13:18
Object.observe(this.data_, this.dataChanged_.bind(
michaelpg
2015/08/26 06:02:27
Object.observe is being deprecated
| |
| 35 } | |
| 36 | |
| 37 // Check if the values are different. | |
| 38 Pref.prototype.equals = function(oldValue, newValue) { | |
|
Dan Beam
2015/08/21 17:13:19
Pref.prototype.equals = function(otherPref) {
re
michaelpg
2015/08/26 06:02:26
It's comparing PrefState and PrefObject, so I thin
| |
| 39 return oldValue == newValue; | |
| 40 }; | |
| 41 | |
| 42 /** | |
| 43 * Update the pref with the value of prefObj when set by settingsPrivate. | |
| 44 * @return {boolean} True if the value has changed since last set by Polymer. | |
| 45 */ | |
| 46 Pref.prototype.update = function(prefObj) { | |
| 47 // TODO: separate. | |
| 48 // | |
| 49 this.cachedValue = prefObj.value; | |
| 50 if (this.equals(this.value, prefObj.value)) | |
|
Dan Beam
2015/08/21 17:13:18
if (this.equals(prefObj))
| |
| 51 return false; | |
| 52 | |
| 53 this.value = prefObj.value; | |
| 54 return true; | |
| 55 }; | |
| 56 | |
| 57 /** | |
| 58 * Registers an observer on the PrefObject using Object.observe. | |
| 59 * @param {!PrefObject} prefObj The preference to observe. | |
| 60 */ | |
| 61 Pref.prototype.observe = function(prefObj) { | |
|
Dan Beam
2015/08/21 17:13:18
Pref.prototype.observe_ = function() {
Object.ob
| |
| 62 Object.observe(prefObj, this.prefChangeCallback_.bind(this), | |
| 63 ['update']); | |
| 64 }; | |
| 65 | |
| 66 /** | |
| 67 * Called when a property of a pref changes. | |
| 68 * @param {!Array<!{object: PrefObject, name: string}>} changes | |
| 69 * Array of change records. | |
| 70 * @private | |
| 71 */ | |
| 72 Pref.prototype.prefChangeCallback_ = function(changes) { | |
| 73 // Rarely, multiple changes could refer to the same value (e.g., toggling | |
| 74 // a boolean preference twice), causing redundant work. We could ignore | |
| 75 // all but the last such change, but that would muddle the data flow. | |
| 76 changes.forEach(function(change) { | |
| 77 // UI should only be able to change the value of a preference. | |
| 78 assert(change.name == 'value'); | |
| 79 let newValue = change.object.value; | |
| 80 assert(newValue != undefined); | |
| 81 | |
| 82 // Update the known value for the sake of this.update. | |
| 83 this.value = newValue; | |
| 84 // (this isn't observed, the original prefObj in prefStore is) | |
| 85 | |
| 86 // If settingsPrivate already has this value, do nothing. (Otherwise, | |
| 87 // a change event from settingsPrivate could make us call | |
| 88 // settingsPrivate.setPref. This can start an infinite IPC loop if there | |
| 89 // are multiple Settings windows and the pref is changed.) | |
| 90 // todo | |
| 91 if (this.equals(this.cachedValue, newValue)) | |
| 92 return; | |
| 93 | |
| 94 chrome.settingsPrivate.setPref( | |
| 95 this.key, | |
| 96 newValue, | |
| 97 /* pageId */ '', | |
| 98 /* callback */ verifySetPref); | |
| 99 }, this); | |
| 100 }; | |
| 101 | |
| 102 Pref.prototype.observeValue = function(prefObj) { | |
| 103 // prefObj is already observed, and value is a primitive. | |
| 104 }; | |
| 105 | |
| 106 /** | |
| 107 * @constructor | |
| 108 * @extends {ListPref} | |
| 109 * @param {!PrefObject} prefObj | |
| 110 */ | |
| 111 function ListPref(prefObj) { | |
| 112 List.call(this, prefObj); | |
|
Dan Beam
2015/08/21 17:13:19
Pref.call?
michaelpg
2015/08/26 06:02:26
Changed.
| |
| 113 } | |
| 114 | |
| 115 ListPref.__proto__ = Pref.prototype; | |
|
Dan Beam
2015/08/21 17:13:19
nit:
ListPref.prototype = {
__proto__: Pref
michaelpg
2015/08/26 06:02:27
OK. I'll take a closer look at this tomorrow.
| |
| 116 | |
| 117 /** @override */ | |
| 118 ListPref.prototype.equals = function(oldValue, newValue) { | |
| 119 if (this.type == chrome.settingsPrivate.PrefType.LIST) { | |
| 120 // Two arrays might have the "same" values, so don't just use "==". | |
| 121 return this.arraysEqual_(oldValue, newValue); | |
| 122 } | |
| 123 }; | |
| 124 | |
| 125 /** @override */ | |
| 126 ListPref.prototype.observeValue = function(prefObj) { | |
| 127 // If the value changed to a new array, observe the new array. | |
| 128 Object.observe(prefObj.value, | |
| 129 this.valueChangeCallback_.bind(this, this.key), | |
| 130 ['update', 'splice']); | |
| 131 }; | |
| 132 | |
| 133 /** @override */ | |
| 134 ListPref.prototype.update = function(prefObj) { | |
| 135 // Retain a copy of the array for settingsPrivate comparison purposes. | |
| 136 this.cachedValue = prefObj.value.slice(); | |
| 137 | |
| 138 // functional equivalence | |
| 139 var changed = this.equals(this.value, prefObj.value); | |
| 140 // identity | |
| 141 this.value = prefObj.value; | |
| 142 return changed; | |
| 143 }; | |
| 144 | |
| 145 /** | |
| 146 * Called when the array value is changed. | |
| 147 * @param {!Array<{object: !Array}>} changes The list of change records. | |
| 148 */ | |
| 149 Pref.prototype.valueChangeCallback_ = function(changes) { | |
| 150 // The underlying array is the same for all changes. | |
| 151 var value = changes[0].object; | |
| 152 // TODO: the array could no longer be the set property... | |
| 153 | |
| 154 // this.value should already refer to the prefObj.value in prefStore, | |
| 155 // so nothing to do here | |
| 156 // (if the value itself is set to a different array, we'll have observed | |
| 157 // that and handled it in ListPref.prototype.prefChangeCallback) | |
| 158 // TODO: verify setPref won't be called in both functions (the functions | |
| 159 // can't be called on the same change) | |
| 160 | |
| 161 // If settingsPrivate already has this value, do nothing. | |
| 162 if (this.arraysEqual_(this.cachedValue, value)) | |
| 163 return; | |
| 164 chrome.settingsPrivate.setPref(this.key, value, | |
| 165 /* pageId */ '', | |
| 166 /* callback */ verifySetPref); | |
| 167 }; | |
| 168 | |
| 169 /** | |
| 170 * Tests whether two arrays contain the same data (true if primitive | |
| 171 * elements are equal and array elements contain the same data). | |
| 172 * @param {Array} arr1 | |
| 173 * @param {Array} arr2 | |
| 174 * @return {boolean} True if the arrays contain similar values. | |
| 175 */ | |
| 176 ListPref.prototype.arraysEqual_ = function(arr1, arr2) { | |
|
Dan Beam
2015/08/21 17:13:19
maybe crazy question, but could ListPref contain a
michaelpg
2015/08/26 06:02:26
Take a look at the new CL and let me know what you
| |
| 177 if (!arr1 || !arr2) | |
| 178 return arr1 == arr2; | |
| 179 if (arr1.length != arr2.length) | |
| 180 return false; | |
| 181 for (let i = 0; i < arr1.length; i++) { | |
| 182 var val1 = arr1[i]; | |
| 183 var val2 = arr2[i]; | |
| 184 assert(typeof val1 != 'object' && typeof val2 != 'object', | |
| 185 'Objects are not supported.'); | |
| 186 if (val1 instanceof Array && !this.arraysEqual_(val1, val2)) | |
| 187 return false; | |
| 188 else if (val1 != val2) | |
| 189 return false; | |
| 190 } | |
| 191 return true; | |
| 192 }; | |
| 193 | |
| 22 Polymer({ | 194 Polymer({ |
| 23 is: 'cr-settings-prefs', | 195 is: 'cr-settings-prefs', |
| 24 | 196 |
| 25 properties: { | 197 properties: { |
| 26 /** | 198 /** |
| 27 * Object containing all preferences. | 199 * Object containing all preferences. |
| 28 */ | 200 */ |
| 29 prefStore: { | 201 prefStore: { |
| 30 type: Object, | 202 type: Object, |
| 31 value: function() { return {}; }, | 203 value: function() { return {}; }, |
| 32 notify: true, | 204 notify: true, |
| 33 }, | 205 }, |
| 34 }, | 206 }, |
| 35 | 207 |
| 36 /** @override */ | 208 /** @override */ |
| 37 created: function() { | 209 created: function() { |
| 38 CrSettingsPrefs.isInitialized = false; | 210 CrSettingsPrefs.isInitialized = false; |
| 211 // Private counterpart to prefStore, used to interact with | |
| 212 // settingsPrivate. | |
| 213 this.prefCache_ = {}; | |
|
Dan Beam
2015/08/21 17:13:18
nit: this.prefs_ or this.state_ if this is long-li
michaelpg
2015/08/26 06:02:27
-> this.settingsPrivateState_
| |
| 39 | 214 |
| 40 chrome.settingsPrivate.onPrefsChanged.addListener( | 215 chrome.settingsPrivate.onPrefsChanged.addListener( |
| 41 this.onPrefsChanged_.bind(this)); | 216 this.onPrefsChanged_.bind(this)); |
| 42 chrome.settingsPrivate.getAllPrefs(this.onPrefsFetched_.bind(this)); | 217 chrome.settingsPrivate.getAllPrefs(this.onPrefsFetched_.bind(this)); |
| 43 }, | 218 }, |
| 44 | 219 |
| 45 /** | 220 /** |
| 46 * Called when prefs in the underlying Chrome pref store are changed. | 221 * Called when prefs in the underlying Chrome pref store are changed. |
| 47 * @param {!Array<!PrefObject>} prefs The prefs that changed. | 222 * @param {!Array<!PrefObject>} prefs The prefs that changed. |
| 48 * @private | 223 * @private |
| 49 */ | 224 */ |
| 50 onPrefsChanged_: function(prefs) { | 225 onPrefsChanged_: function(prefs) { |
| 51 this.updatePrefs_(prefs, false); | 226 if (CrSettingsPrefs.isInitialized) |
| 227 this.updatePrefs_(prefs); | |
| 52 }, | 228 }, |
| 53 | 229 |
| 54 /** | 230 /** |
| 55 * Called when prefs are fetched from settingsPrivate. | 231 * Called when prefs are fetched from settingsPrivate. |
| 56 * @param {!Array<!PrefObject>} prefs | 232 * @param {!Array<!PrefObject>} prefs |
| 57 * @private | 233 * @private |
| 58 */ | 234 */ |
| 59 onPrefsFetched_: function(prefs) { | 235 onPrefsFetched_: function(prefs) { |
| 60 this.updatePrefs_(prefs, true); | 236 this.updatePrefs_(prefs); |
| 61 | 237 |
| 62 CrSettingsPrefs.isInitialized = true; | 238 CrSettingsPrefs.isInitialized = true; |
| 63 document.dispatchEvent(new Event(CrSettingsPrefs.INITIALIZED)); | 239 document.dispatchEvent(new Event(CrSettingsPrefs.INITIALIZED)); |
| 64 }, | 240 }, |
| 65 | 241 |
| 66 | |
| 67 /** | 242 /** |
| 68 * Updates the settings model with the given prefs. | 243 * Updates the prefs model with the given prefs. |
| 69 * @param {!Array<!PrefObject>} prefs | 244 * @param {!Array<!PrefObject>} prefs |
| 70 * @param {boolean} shouldObserve Whether each of the prefs should be | |
| 71 * observed. | |
| 72 * @private | 245 * @private |
| 73 */ | 246 */ |
| 74 updatePrefs_: function(prefs, shouldObserve) { | 247 updatePrefs_: function(prefs) { |
| 75 prefs.forEach(function(prefObj) { | 248 prefs.forEach(function(prefObj) { |
| 76 let root = this.prefStore; | 249 var cachedPref = this.getCachedPref_(prefObj); |
| 77 let tokens = prefObj.key.split('.'); | |
| 78 | 250 |
| 79 assert(tokens.length > 0); | 251 // Cache a copy of the value from settingsPrivate. |
| 252 var changed = cachedPref.update(prefObj); | |
| 80 | 253 |
| 81 for (let i = 0; i < tokens.length; i++) { | 254 // Check if the pref exists already in the Polymer pref object. |
| 82 let token = tokens[i]; | 255 var pref = /** @type {(!PrefObject|undefined)} */( |
| 256 this.get(prefObj.key, this.prefStore)); | |
| 257 if (pref) { | |
| 258 if (cachedPref.equals(prefObj)) | |
| 259 return; | |
| 83 | 260 |
| 84 if (!root.hasOwnProperty(token)) { | 261 cachedPref.observeValue(prefObj); |
| 85 let path = 'prefStore.' + tokens.slice(0, i + 1).join('.'); | 262 this.set('prefStore.' + pref.key + '.value', prefObj.value); |
| 86 this.set(path, {}); | 263 } else { |
| 87 } | 264 let node = this.prefStore; |
| 88 root = root[token]; | 265 let tokens = prefObj.key.split('.').slice(0, -1); |
| 89 } | |
| 90 | 266 |
| 91 // NOTE: Do this copy rather than just a re-assignment, so that the | 267 // Crawl the pref tree, generating objects where necessary. |
| 92 // observer fires. | 268 tokens.forEach(function(token) { |
| 93 for (let objKey in prefObj) { | 269 if (!node.hasOwnProperty(token)) { |
| 94 let path = 'prefStore.' + prefObj.key + '.' + objKey; | 270 // Don't use Polymer.Base.set because the property update events |
| 271 // won't be useful until the actual pref is set below. | |
| 272 node[token] = {}; | |
| 273 } | |
| 274 node = node[token]; | |
| 275 }, this); | |
| 95 | 276 |
| 96 // Handle lists specially. We don't want to call this.set() | 277 // Register the observer before this.set(); setting the pref in the |
| 97 // unconditionally upon updating a list value, since even its contents | 278 // prefStore could trigger a change in prefObj as a side effect. |
| 98 // are the same as the old list, doing this set() may cause an | 279 cachedPref.observe(prefObj); |
| 99 // infinite update cycle (http://crbug.com/498586). | 280 this.set('prefStore.' + prefObj.key, prefObj); |
| 100 if (objKey == 'value' && | |
| 101 prefObj.type == chrome.settingsPrivate.PrefType.LIST && | |
| 102 !this.shouldUpdateListPrefValue_(root, prefObj['value'])) { | |
| 103 continue; | |
| 104 } | |
| 105 | |
| 106 this.set(path, prefObj[objKey]); | |
| 107 } | |
| 108 | |
| 109 if (shouldObserve) { | |
| 110 Object.observe(root, this.propertyChangeCallback_, ['update']); | |
| 111 } | 281 } |
| 112 }, this); | 282 }, this); |
| 113 }, | 283 }, |
| 114 | 284 |
| 285 /** | |
| 286 * @param {!PrefObject} prefObj | |
| 287 */ | |
| 288 getCachedPref_: function(prefObj) { | |
| 289 var cachedPref = this.prefs_[prefObj.key]; | |
| 290 if (cachedPref) | |
| 291 return cachedPref; | |
| 115 | 292 |
| 116 /** | 293 // Create a cached pref for the PrefObject. |
| 117 * @param {Object} root The root object for a pref that contains a list | 294 if (prefObj.type == chrome.settingsPrivate.PrefType.LIST) |
| 118 * value. | 295 cachedPref = new ListPref(prefObj); |
| 119 * @param {!Array} newValue The new list value. | 296 cachedPref = new Pref(prefObj); |
| 120 * @return {boolean} Whether the new value is different from the one in | |
| 121 * root, thus necessitating a pref update. | |
| 122 */ | |
| 123 shouldUpdateListPrefValue_: function(root, newValue) { | |
| 124 if (root.value == null || | |
| 125 root.value.length != newValue.length) { | |
| 126 return true; | |
| 127 } | |
| 128 | 297 |
| 129 for (let i = 0; i < newValue.length; i++) { | 298 // Set the cached pref. |
| 130 if (root.value != null && root.value[i] != newValue[i]) { | 299 this.prefs_[prefObj.key] = cachedPref; |
| 131 return true; | 300 return cachedPref; |
| 132 } | |
| 133 } | |
| 134 | |
| 135 return false; | |
| 136 }, | 301 }, |
| 137 | 302 |
| 138 /** | |
| 139 * Called when a property of a pref changes. | |
| 140 * @param {!Array<!Object>} changes An array of objects describing changes. | |
| 141 * @see http://www.html5rocks.com/en/tutorials/es7/observe/ | |
| 142 * @private | |
| 143 */ | |
| 144 propertyChangeCallback_: function(changes) { | |
| 145 changes.forEach(function(change) { | |
| 146 // UI should only be able to change the value of a setting for now, not | |
| 147 // disabled, etc. | |
| 148 assert(change.name == 'value'); | |
| 149 | |
| 150 let newValue = change.object[change.name]; | |
| 151 assert(newValue !== undefined); | |
| 152 | |
| 153 chrome.settingsPrivate.setPref( | |
| 154 change.object['key'], | |
| 155 newValue, | |
| 156 /* pageId */ '', | |
| 157 /* callback */ function() {}); | |
| 158 }); | |
| 159 }, | |
| 160 }); | 303 }); |
| 161 })(); | 304 })(); |
| OLD | NEW |