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' models Chrome settings and preferences, listening for |
| 8 * interaction with settings which are stored in Chrome's | 8 * changes to Chrome prefs whitelisted in chrome.settingsPrivate. |
| 9 * Preferences. | 9 * When changing prefs in this element's 'prefs' property via the UI, this |
| 10 * element tries to set those preferences in Chrome. Whether or not the calls to | |
| 11 * settingsPrivate.setPref succeed, 'prefs' is eventually consistent with the | |
| 12 * Chrome pref store. | |
| 10 * | 13 * |
| 11 * Example: | 14 * Example: |
| 12 * | 15 * |
| 13 * <cr-settings-prefs id="prefs"></cr-settings-prefs> | 16 * <cr-settings-prefs prefs="{{prefs}}"></cr-settings-prefs> |
| 14 * <cr-settings-a11y-page prefs="{{this.$.prefs}}"></cr-settings-a11y-page> | 17 * <cr-settings-a11y-page prefs="{{prefs}}"></cr-settings-a11y-page> |
| 15 * | 18 * |
| 16 * @group Chrome Settings Elements | 19 * @group Chrome Settings Elements |
| 17 * @element cr-settings-a11y-page | 20 * @element cr-settings-prefs |
| 18 */ | 21 */ |
| 19 (function() { | 22 (function() { |
| 20 'use strict'; | 23 'use strict'; |
| 21 | 24 |
| 25 /** | |
| 26 * Pref state object. Copies values of PrefObjects received from | |
| 27 * settingsPrivate to determine when pref values have changed. | |
| 28 * This prototype works for primitive types, but more complex types should | |
| 29 * override these functions. | |
| 30 * @constructor | |
|
Dan Beam
2015/08/28 00:08:41
this might be better as an interface if we can't f
michaelpg
2015/08/28 01:52:47
?
the sane default is == (for all primitive types
| |
| 31 * @param {!PrefObject} prefObj | |
|
Dan Beam
2015/08/26 16:51:33
PrefObject -> PrefData
stevenjb
2015/08/26 17:32:18
What is PrefData? PrefObject is from settingsPriva
Dan Beam
2015/08/26 17:38:01
it's my proposition for a better name
stevenjb
2015/08/26 18:07:09
My point is that't you're proposing a change to an
michaelpg
2015/08/26 23:32:15
I don't see how PrefObject provides more informati
| |
| 32 */ | |
| 33 function PrefState(prefObj) { | |
|
Dan Beam
2015/08/26 16:51:33
PrefState -> Pref, PrefWrapper
michaelpg
2015/08/26 23:32:15
Done: PrefState -> PrefWrapper.
| |
| 34 this.value = prefObj.value; | |
|
Dan Beam
2015/08/28 00:08:40
why does this have to be public?
michaelpg
2015/08/28 01:52:48
Doesn't. Done.
| |
| 35 } | |
| 36 | |
| 37 /** | |
| 38 * Checks if the value matches this pref's value. | |
| 39 * @param {*} value | |
| 40 * @return {boolean} | |
| 41 */ | |
| 42 PrefState.prototype.equalsValue = function(value) { | |
|
Dan Beam
2015/08/26 16:51:33
PrefState.prototype.equals = function(other) {
r
michaelpg
2015/08/26 23:32:15
As I mentioned in a previous comment, PrefState (P
Dan Beam
2015/08/28 00:08:40
construct a PrefWrapper() around the PrefState and
michaelpg
2015/08/28 01:52:48
Done. I didn't have that because it seemed like ex
| |
| 43 return this.value == value; | |
|
Dan Beam
2015/08/28 00:08:40
how do we prevent PrefStates of different types fr
michaelpg
2015/08/28 01:52:48
Prefs are typed in their PrefRegistry as a specifi
| |
| 44 }; | |
| 45 | |
| 46 /** | |
| 47 * @constructor | |
| 48 * @extends {PrefState} | |
| 49 * @param {!PrefObject} prefObj | |
| 50 */ | |
| 51 function ListPrefState(prefObj) { | |
| 52 // Copy the array so changes to prefObj aren't reflected in this.value. | |
| 53 // TODO(michaelpg): Do a deep copy to support nested lists and objects. | |
| 54 this.value = prefObj.value.slice(); | |
| 55 } | |
| 56 | |
| 57 ListPrefState.__proto__ = PrefState.prototype; | |
|
Dan Beam
2015/08/26 16:51:33
ListPrefState.prototype = {
__proto__: PrefState
michaelpg
2015/08/26 23:32:15
Done.
| |
| 58 | |
| 59 /** @override */ | |
| 60 ListPrefState.prototype.equalsValue = function(value) { | |
| 61 // Two arrays might have the same values, so don't just use "==". | |
| 62 return this.arraysEqual_(this.value, value); | |
| 63 }; | |
| 64 | |
| 65 /** | |
| 66 * Tests whether two arrays contain the same data (true if primitive | |
| 67 * elements are equal and array elements contain the same data). | |
| 68 * @param {Array} arr1 | |
| 69 * @param {Array} arr2 | |
| 70 * @return {boolean} True if the arrays contain similar values. | |
| 71 */ | |
| 72 ListPrefState.prototype.arraysEqual_ = function(arr1, arr2) { | |
|
Dan Beam
2015/08/28 00:08:40
could this be generalized to work for primitives a
| |
| 73 if (!arr1 || !arr2) | |
|
Dan Beam
2015/08/26 16:51:33
empty arrays are truthy, btw
is it valid to get h
stevenjb
2015/08/26 17:32:18
PrefObject.value should never be null, so I believ
michaelpg
2015/08/26 23:32:15
Seems unlikely but it depends on how prefs are str
Dan Beam
2015/08/28 00:08:40
this opens a lot of holes. 0 == [''] for example
| |
| 74 return arr1 == arr2; | |
| 75 if (arr1.length != arr2.length) | |
| 76 return false; | |
| 77 for (let i = 0; i < arr1.length; i++) { | |
| 78 var val1 = arr1[i]; | |
| 79 var val2 = arr2[i]; | |
| 80 assert(typeof val1 != 'object' && typeof val2 != 'object', | |
| 81 'Objects are not supported.'); | |
| 82 if (val1 instanceof Array && !this.arraysEqual_(val1, val2)) | |
|
Dan Beam
2015/08/26 16:51:33
nit: Array.isArray(val1)
michaelpg
2015/08/26 23:32:15
Done.
| |
| 83 return false; | |
| 84 else if (val1 != val2) | |
| 85 return false; | |
| 86 } | |
| 87 return true; | |
|
Dan Beam
2015/08/26 16:51:33
why not
return arr1.join() == arr2.join();
if
stevenjb
2015/08/26 17:32:18
The C++ coder in me says "Eww". :) I know it's pre
michaelpg
2015/08/26 23:32:15
Not sure what you're going for: the second isn't a
michaelpg
2015/08/26 23:32:15
JSON.stringify doesn't guarantee ordering of objec
Dan Beam
2015/08/28 00:08:40
how does your current code handle arrays of dictio
michaelpg
2015/08/28 01:52:47
It doesn't. So I'll remove this and we can restart
| |
| 88 }; | |
| 89 | |
| 22 Polymer({ | 90 Polymer({ |
| 23 is: 'cr-settings-prefs', | 91 is: 'cr-settings-prefs', |
| 24 | 92 |
| 25 properties: { | 93 properties: { |
| 26 /** | 94 /** |
| 27 * Object containing all preferences. | 95 * Object containing all preferences, for use by Polymer controls. |
| 28 */ | 96 */ |
| 29 prefStore: { | 97 prefs: { |
| 30 type: Object, | 98 type: Object, |
| 31 value: function() { return {}; }, | 99 value: function() { return {}; }, |
| 32 notify: true, | 100 notify: true, |
| 33 }, | 101 }, |
| 102 | |
| 103 /** | |
| 104 * Map of pref keys to PrefState objects representing the state of the | |
| 105 * Chrome pref store. | |
| 106 * @type {Object<PrefState>} | |
| 107 */ | |
| 108 settingsPrivateState_: { | |
| 109 type: Object, | |
| 110 value: function() { return {}; }, | |
| 111 }, | |
| 34 }, | 112 }, |
| 35 | 113 |
| 114 observers: [ | |
| 115 'prefsChanged_(prefs.*)', | |
| 116 ], | |
| 117 | |
| 36 /** @override */ | 118 /** @override */ |
| 37 created: function() { | 119 created: function() { |
| 38 CrSettingsPrefs.isInitialized = false; | 120 CrSettingsPrefs.isInitialized = false; |
| 39 | 121 |
| 40 chrome.settingsPrivate.onPrefsChanged.addListener( | 122 chrome.settingsPrivate.onPrefsChanged.addListener( |
| 41 this.onPrefsChanged_.bind(this)); | 123 this.onSettingsPrivatePrefsChanged_.bind(this)); |
| 42 chrome.settingsPrivate.getAllPrefs(this.onPrefsFetched_.bind(this)); | 124 chrome.settingsPrivate.getAllPrefs( |
| 125 this.onSettingsPrivatePrefsFetched_.bind(this)); | |
| 43 }, | 126 }, |
| 44 | 127 |
| 45 /** | 128 /** |
| 129 * Polymer callback for changes to this.prefs. | |
| 130 * @param {!{path: string, value: *}} change | |
| 131 */ | |
| 132 prefsChanged_: function(change) { | |
| 133 if (!CrSettingsPrefs.isInitialized) | |
| 134 return; | |
| 135 | |
| 136 var key = this.getPrefKeyFromPath_(change.path); | |
| 137 var prefState = this.settingsPrivateState_[key]; | |
| 138 if (!prefState) | |
| 139 return; | |
| 140 var prefObj = this.get(key, this.prefs); | |
| 141 | |
| 142 // If settingsPrivate already has this value, do nothing. (Otherwise, | |
| 143 // a change event from settingsPrivate could make us call | |
| 144 // settingsPrivate.setPref and potentially trigger an IPC loop.) | |
| 145 if (!prefState.equalsValue(prefObj.value)) { | |
|
stevenjb
2015/08/26 17:32:18
nit: Early exit would better match the comment.
michaelpg
2015/08/26 23:32:15
Done.
| |
| 146 chrome.settingsPrivate.setPref( | |
| 147 key, | |
| 148 prefObj.value, | |
| 149 /* pageId */ '', | |
| 150 /* callback */ this.setPrefCallback_.bind(this, key)); | |
| 151 } | |
| 152 }, | |
| 153 | |
| 154 /** | |
| 46 * Called when prefs in the underlying Chrome pref store are changed. | 155 * Called when prefs in the underlying Chrome pref store are changed. |
| 47 * @param {!Array<!PrefObject>} prefs The prefs that changed. | 156 * @param {!Array<!PrefObject>} prefs The prefs that changed. |
| 48 * @private | 157 * @private |
| 49 */ | 158 */ |
| 50 onPrefsChanged_: function(prefs) { | 159 onSettingsPrivatePrefsChanged_: function(prefs) { |
| 51 this.updatePrefs_(prefs, false); | 160 if (CrSettingsPrefs.isInitialized) |
| 161 this.updatePrefs_(prefs); | |
| 52 }, | 162 }, |
| 53 | 163 |
| 54 /** | 164 /** |
| 55 * Called when prefs are fetched from settingsPrivate. | 165 * Called when prefs are fetched from settingsPrivate. |
| 56 * @param {!Array<!PrefObject>} prefs | 166 * @param {!Array<!PrefObject>} prefs |
| 57 * @private | 167 * @private |
| 58 */ | 168 */ |
| 59 onPrefsFetched_: function(prefs) { | 169 onSettingsPrivatePrefsFetched_: function(prefs) { |
| 60 this.updatePrefs_(prefs, true); | 170 this.updatePrefs_(prefs); |
| 61 | 171 |
| 62 CrSettingsPrefs.isInitialized = true; | 172 CrSettingsPrefs.isInitialized = true; |
| 63 document.dispatchEvent(new Event(CrSettingsPrefs.INITIALIZED)); | 173 document.dispatchEvent(new Event(CrSettingsPrefs.INITIALIZED)); |
| 64 }, | 174 }, |
| 65 | 175 |
| 176 /** | |
| 177 * Checks the result of calling settingsPrivate.setPref. | |
| 178 * @param {string} key The key used in the call to setPref. | |
| 179 * @param {boolean} success True if setting the pref succeeded. | |
| 180 */ | |
| 181 setPrefCallback_: function(key, success) { | |
| 182 if (!success) { | |
|
Dan Beam
2015/08/26 16:51:33
if (success)
return;
...
michaelpg
2015/08/26 23:32:15
Done.
| |
| 183 // Get the current pref value from chrome.settingsPrivate to ensure the | |
| 184 // UI stays up to date. | |
| 185 chrome.settingsPrivate.getPref( | |
| 186 key, | |
| 187 function(pref) { | |
| 188 assert(pref); | |
| 189 this.updatePrefs_([pref]); | |
| 190 }.bind(this)); | |
|
Dan Beam
2015/08/26 16:51:33
chrome.settingsPrivate.getPref(key, function(pref)
michaelpg
2015/08/26 23:32:15
I don't think that's more readable but it's more c
| |
| 191 } | |
| 192 }, | |
| 66 | 193 |
| 67 /** | 194 /** |
| 68 * Updates the settings model with the given prefs. | 195 * Updates the prefs model with the given prefs. |
| 69 * @param {!Array<!PrefObject>} prefs | 196 * @param {!Array<!PrefObject>} prefs |
| 70 * @param {boolean} shouldObserve Whether each of the prefs should be | |
| 71 * observed. | |
| 72 * @private | 197 * @private |
| 73 */ | 198 */ |
| 74 updatePrefs_: function(prefs, shouldObserve) { | 199 updatePrefs_: function(prefs) { |
| 75 prefs.forEach(function(prefObj) { | 200 prefs.forEach(function(newPrefObj) { |
| 76 let root = this.prefStore; | 201 // Set or update the PrefState in settingsPrivateState_ with the |
| 77 let tokens = prefObj.key.split('.'); | 202 // PrefObject from settingsPrivate. |
| 203 this.setPrefState_(newPrefObj); | |
| 78 | 204 |
| 79 assert(tokens.length > 0); | 205 // Set or update the pref in |prefs|. This triggers observers in the UI, |
| 80 | 206 // which update controls associated with the pref. |
| 81 for (let i = 0; i < tokens.length; i++) { | 207 this.setPref_(newPrefObj); |
| 82 let token = tokens[i]; | |
| 83 | |
| 84 if (!root.hasOwnProperty(token)) { | |
| 85 let path = 'prefStore.' + tokens.slice(0, i + 1).join('.'); | |
| 86 this.set(path, {}); | |
| 87 } | |
| 88 root = root[token]; | |
| 89 } | |
| 90 | |
| 91 // NOTE: Do this copy rather than just a re-assignment, so that the | |
| 92 // observer fires. | |
| 93 for (let objKey in prefObj) { | |
| 94 let path = 'prefStore.' + prefObj.key + '.' + objKey; | |
| 95 | |
| 96 // Handle lists specially. We don't want to call this.set() | |
| 97 // unconditionally upon updating a list value, since even its contents | |
| 98 // are the same as the old list, doing this set() may cause an | |
| 99 // infinite update cycle (http://crbug.com/498586). | |
| 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 } | |
| 112 }, this); | 208 }, this); |
| 113 }, | 209 }, |
| 114 | 210 |
| 211 /** | |
| 212 * Returns the key of the preference specified by the path. The path may | |
| 213 * include tokens after the key, e.g. 'prefs.[key].value'. | |
| 214 * @param {string} path | |
| 215 * @return {string} | |
|
Dan Beam
2015/08/26 16:51:33
all _ -> @private
michaelpg
2015/08/26 23:32:15
Done.
| |
| 216 */ | |
| 217 getPrefKeyFromPath_: function(path) { | |
|
Dan Beam
2015/08/26 16:51:33
i have no idea what this does, maybe give some con
michaelpg
2015/08/26 23:32:15
Done.
| |
| 218 var tokens = path.split('.'); | |
|
Dan Beam
2015/08/26 16:51:33
path.split('.').slice(1);
michaelpg
2015/08/26 23:32:15
Done.
| |
| 219 var key = ''; | |
| 115 | 220 |
| 116 /** | 221 // Skip the first token, which refers to the member variable (this.prefs). |
| 117 * @param {Object} root The root object for a pref that contains a list | 222 for (let i = 1; i < tokens.length; i++) { |
| 118 * value. | 223 if (key) |
| 119 * @param {!Array} newValue The new list value. | 224 key += '.'; |
| 120 * @return {boolean} Whether the new value is different from the one in | 225 key += tokens[i]; |
| 121 * root, thus necessitating a pref update. | 226 // The settingsPrivateState_ keys match the pref keys. |
| 122 */ | 227 if (this.settingsPrivateState_[key] != undefined) |
| 123 shouldUpdateListPrefValue_: function(root, newValue) { | 228 return key; |
| 124 if (root.value == null || | |
| 125 root.value.length != newValue.length) { | |
| 126 return true; | |
| 127 } | 229 } |
| 230 return ''; | |
| 231 }, | |
| 128 | 232 |
|
Dan Beam
2015/08/26 16:51:33
doc
michaelpg
2015/08/26 23:32:15
Done.
| |
| 129 for (let i = 0; i < newValue.length; i++) { | 233 setPref_: function(newPrefObj) { |
| 130 if (root.value != null && root.value[i] != newValue[i]) { | 234 // Check if the pref exists already in the Polymer |prefs| object. |
| 131 return true; | 235 if (this.get(newPrefObj.key, this.prefs)) { |
| 132 } | 236 // Update just the value. |
| 237 this.set('prefs.' + newPrefObj.key + '.value', newPrefObj.value); | |
| 238 } else { | |
| 239 // Add the pref to |prefs|. | |
| 240 let node = this.prefs; | |
| 241 let tokens = newPrefObj.key.split('.').slice(0, -1); | |
| 242 | |
| 243 // Crawl the pref tree, generating objects where necessary. | |
| 244 tokens.forEach(function(token) { | |
| 245 if (!node.hasOwnProperty(token)) { | |
| 246 // Don't use Polymer.Base.set because the property update events | |
| 247 // won't be useful until the actual pref is set below. | |
| 248 node[token] = {}; | |
| 249 } | |
| 250 node = node[token]; | |
| 251 }, this); | |
| 252 | |
|
stevenjb
2015/08/26 17:32:18
nit: Add comment that notification will occur here
michaelpg
2015/08/26 23:32:15
Done.
| |
| 253 this.set('prefs.' + newPrefObj.key, newPrefObj); | |
| 133 } | 254 } |
| 134 | |
| 135 return false; | |
| 136 }, | 255 }, |
| 137 | 256 |
| 138 /** | 257 /** |
| 139 * Called when a property of a pref changes. | 258 * Creates a PrefState object from a chrome.settingsPrivate pref and adds it |
| 140 * @param {!Array<!Object>} changes An array of objects describing changes. | 259 * to settingsPrivateState_. |
| 141 * @see http://www.html5rocks.com/en/tutorials/es7/observe/ | 260 * @param {!PrefObject} PrefObject received from chrome.settingsPrivate. |
| 142 * @private | |
| 143 */ | 261 */ |
| 144 propertyChangeCallback_: function(changes) { | 262 setPrefState_: function(prefObj) { |
| 145 changes.forEach(function(change) { | 263 var prefState; |
| 146 // UI should only be able to change the value of a setting for now, not | 264 if (prefObj.type == chrome.settingsPrivate.PrefType.LIST) |
| 147 // disabled, etc. | 265 prefState = new ListPrefState(prefObj); |
| 148 assert(change.name == 'value'); | 266 else |
| 149 | 267 prefState = new PrefState(prefObj); |
|
Dan Beam
2015/08/26 16:51:33
nit: ternary
var prefState = prefObj.type == chro
michaelpg
2015/08/26 23:32:15
If we add object prefs, we'd have to change this b
Dan Beam
2015/08/28 00:08:41
we're not yet. do it then
michaelpg
2015/08/28 01:52:47
Done.
| |
| 150 let newValue = change.object[change.name]; | 268 this.settingsPrivateState_[prefObj.key] = prefState; |
| 151 assert(newValue !== undefined); | |
| 152 | |
| 153 chrome.settingsPrivate.setPref( | |
| 154 change.object['key'], | |
| 155 newValue, | |
| 156 /* pageId */ '', | |
| 157 /* callback */ function() {}); | |
| 158 }); | |
| 159 }, | 269 }, |
| 160 }); | 270 }); |
| 161 })(); | 271 })(); |
| OLD | NEW |