// Copyright 2009 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Definitions for all tweak entries. * The class hierarchy is as follows (abstract entries are denoted with a *): * BaseEntry(id, description) * * -> ButtonAction(buttons in the UI) * -> BaseSetting(query parameter) * * -> BooleanGroup(child booleans) * -> BasePrimitiveSetting(value, defaultValue) * * -> BooleanSetting * -> StringSetting * -> NumericSetting * -> BooleanInGroupSetting(token) * Most clients should not use these classes directly, but instead use the API * defined in tweak.js. One possible use case for directly using them is to * register tweaks that are not known at compile time. * * @author agrieve@google.com (Andrew Grieve) */ goog.provide('goog.tweak.BaseEntry'); goog.provide('goog.tweak.BasePrimitiveSetting'); goog.provide('goog.tweak.BaseSetting'); goog.provide('goog.tweak.BooleanGroup'); goog.provide('goog.tweak.BooleanInGroupSetting'); goog.provide('goog.tweak.BooleanSetting'); goog.provide('goog.tweak.ButtonAction'); goog.provide('goog.tweak.NumericSetting'); goog.provide('goog.tweak.StringSetting'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.log'); goog.require('goog.object'); /** * Base class for all Registry entries. * @param {string} id The ID for the entry. Must contain only letters, * numbers, underscores and periods. * @param {string} description A description of what the entry does. * @constructor */ goog.tweak.BaseEntry = function(id, description) { /** * An ID to uniquely identify the entry. * @type {string} * @private */ this.id_ = id; /** * A descriptive label for the entry. * @type {string} */ this.label = id; /** * A description of what this entry does. * @type {string} */ this.description = description; /** * Functions to be called whenever a setting is changed or a button is * clicked. * @type {!Array} * @private */ this.callbacks_ = []; }; /** * The logger for this class. * @type {goog.log.Logger} * @protected */ goog.tweak.BaseEntry.prototype.logger = goog.log.getLogger('goog.tweak.BaseEntry'); /** * Whether a restart is required for changes to the setting to take effect. * @type {boolean} * @private */ goog.tweak.BaseEntry.prototype.restartRequired_ = true; /** * @return {string} Returns the entry's ID. */ goog.tweak.BaseEntry.prototype.getId = function() { return this.id_; }; /** * Returns whether a restart is required for changes to the setting to take * effect. * @return {boolean} The value. */ goog.tweak.BaseEntry.prototype.isRestartRequired = function() { return this.restartRequired_; }; /** * Sets whether a restart is required for changes to the setting to take * effect. * @param {boolean} value The new value. */ goog.tweak.BaseEntry.prototype.setRestartRequired = function(value) { this.restartRequired_ = value; }; /** * Adds a callback that should be called when the setting has changed (or when * an action has been clicked). * @param {!Function} callback The callback to add. */ goog.tweak.BaseEntry.prototype.addCallback = function(callback) { this.callbacks_.push(callback); }; /** * Removes a callback that was added by addCallback. * @param {!Function} callback The callback to add. */ goog.tweak.BaseEntry.prototype.removeCallback = function(callback) { goog.array.remove(this.callbacks_, callback); }; /** * Calls all registered callbacks. */ goog.tweak.BaseEntry.prototype.fireCallbacks = function() { for (var i = 0, callback; callback = this.callbacks_[i]; ++i) { callback(this); } }; /** * Base class for all tweak entries that are settings. Settings are entries * that are associated with a query parameter. * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @constructor * @extends {goog.tweak.BaseEntry} */ goog.tweak.BaseSetting = function(id, description) { goog.tweak.BaseEntry.call(this, id, description); // Apply this restriction for settings since they turn in to query // parameters. For buttons, it's not really important. goog.asserts.assert( !/[^A-Za-z0-9._]/.test(id), 'Tweak id contains illegal characters: ', id); /** * The value of this setting's query parameter. * @type {string|undefined} * @protected */ this.initialQueryParamValue; /** * The query parameter that controls this setting. * @type {?string} * @private */ this.paramName_ = this.getId().toLowerCase(); }; goog.inherits(goog.tweak.BaseSetting, goog.tweak.BaseEntry); /** * States of initialization. Entries are initialized lazily in order to allow * their initialization to happen in multiple statements. * @enum {number} * @private */ goog.tweak.BaseSetting.InitializeState_ = { // The start state for all settings. NOT_INITIALIZED: 0, // This is used to allow concrete classes to call assertNotInitialized() // during their initialize() function. INITIALIZING: 1, // One a setting is initialized, it may no longer change its configuration // settings (associated query parameter, token, etc). INITIALIZED: 2 }; /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.BaseSetting.prototype.logger = goog.log.getLogger('goog.tweak.BaseSetting'); /** * Whether initialize() has been called (or is in the middle of being called). * @type {goog.tweak.BaseSetting.InitializeState_} * @private */ goog.tweak.BaseSetting.prototype.initializeState_ = goog.tweak.BaseSetting.InitializeState_.NOT_INITIALIZED; /** * Sets the value of the entry based on the value of the query parameter. Once * this is called, configuration settings (associated query parameter, token, * etc) may not be changed. * @param {?string} value The part of the query param for this setting after * the '='. Null if it is not present. * @protected */ goog.tweak.BaseSetting.prototype.initialize = goog.abstractMethod; /** * Returns the value to be used in the query parameter for this tweak. * @return {?string} The encoded value. Null if the value is set to its * default. */ goog.tweak.BaseSetting.prototype.getNewValueEncoded = goog.abstractMethod; /** * Asserts that this tweak has not been initialized yet. * @param {string} funcName Function name to use in the assertion message. * @protected */ goog.tweak.BaseSetting.prototype.assertNotInitialized = function(funcName) { goog.asserts.assert( this.initializeState_ != goog.tweak.BaseSetting.InitializeState_.INITIALIZED, 'Cannot call ' + funcName + ' after the tweak as been initialized.'); }; /** * Returns whether the setting is currently being initialized. * @return {boolean} Whether the setting is currently being initialized. * @protected */ goog.tweak.BaseSetting.prototype.isInitializing = function() { return this.initializeState_ == goog.tweak.BaseSetting.InitializeState_.INITIALIZING; }; /** * Sets the initial query parameter value for this setting. May not be called * after the setting has been initialized. * @param {string} value The initial query parameter value for this setting. */ goog.tweak.BaseSetting.prototype.setInitialQueryParamValue = function(value) { this.assertNotInitialized('setInitialQueryParamValue'); this.initialQueryParamValue = value; }; /** * Returns the name of the query parameter used for this setting. * @return {?string} The param name. Null if no query parameter is directly * associated with the setting. */ goog.tweak.BaseSetting.prototype.getParamName = function() { return this.paramName_; }; /** * Sets the name of the query parameter used for this setting. If null is * passed the the setting will not appear in the top-level query string. * @param {?string} value The new value. */ goog.tweak.BaseSetting.prototype.setParamName = function(value) { this.assertNotInitialized('setParamName'); this.paramName_ = value; }; /** * Applies the default value or query param value if this is the first time * that the function has been called. * @protected */ goog.tweak.BaseSetting.prototype.ensureInitialized = function() { if (this.initializeState_ == goog.tweak.BaseSetting.InitializeState_.NOT_INITIALIZED) { // Instead of having only initialized / not initialized, there is a // separate in-between state so that functions that call // assertNotInitialized() will not fail when called inside of the // initialize(). this.initializeState_ = goog.tweak.BaseSetting.InitializeState_.INITIALIZING; var value = this.initialQueryParamValue == undefined ? null : this.initialQueryParamValue; this.initialize(value); this.initializeState_ = goog.tweak.BaseSetting.InitializeState_.INITIALIZED; } }; /** * Base class for all settings that wrap primitive values. * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @param {*} defaultValue The default value for this setting. * @constructor * @extends {goog.tweak.BaseSetting} */ goog.tweak.BasePrimitiveSetting = function(id, description, defaultValue) { goog.tweak.BaseSetting.call(this, id, description); /** * The default value of the setting. * @type {*} * @private */ this.defaultValue_ = defaultValue; /** * The value of the tweak. * @type {*} * @private */ this.value_; /** * The value of the tweak once "Apply Tweaks" is pressed. * @type {*} * @private */ this.newValue_; }; goog.inherits(goog.tweak.BasePrimitiveSetting, goog.tweak.BaseSetting); /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.BasePrimitiveSetting.prototype.logger = goog.log.getLogger('goog.tweak.BasePrimitiveSetting'); /** * Returns the query param encoded representation of the setting's value. * @return {string} The encoded value. * @protected */ goog.tweak.BasePrimitiveSetting.prototype.encodeNewValue = goog.abstractMethod; /** * If the setting has the restartRequired option, then returns its initial * value. Otherwise, returns its current value. * @return {*} The value. */ goog.tweak.BasePrimitiveSetting.prototype.getValue = function() { this.ensureInitialized(); return this.value_; }; /** * Returns the value of the setting to use once "Apply Tweaks" is clicked. * @return {*} The value. */ goog.tweak.BasePrimitiveSetting.prototype.getNewValue = function() { this.ensureInitialized(); return this.newValue_; }; /** * Sets the value of the setting. If the setting has the restartRequired * option, then the value will not be changed until the "Apply Tweaks" button * is clicked. If it does not have the option, the value will be update * immediately and all registered callbacks will be called. * @param {*} value The value. */ goog.tweak.BasePrimitiveSetting.prototype.setValue = function(value) { this.ensureInitialized(); var changed = this.newValue_ != value; this.newValue_ = value; // Don't fire callbacks if we are currently in the initialize() method. if (this.isInitializing()) { this.value_ = value; } else { if (!this.isRestartRequired()) { // Update the current value only if the tweak has been marked as not // needing a restart. this.value_ = value; } if (changed) { this.fireCallbacks(); } } }; /** * Returns the default value for this setting. * @return {*} The default value. */ goog.tweak.BasePrimitiveSetting.prototype.getDefaultValue = function() { return this.defaultValue_; }; /** * Sets the default value for the tweak. * @param {*} value The new value. */ goog.tweak.BasePrimitiveSetting.prototype.setDefaultValue = function(value) { this.assertNotInitialized('setDefaultValue'); this.defaultValue_ = value; }; /** * @override */ goog.tweak.BasePrimitiveSetting.prototype.getNewValueEncoded = function() { this.ensureInitialized(); return this.newValue_ == this.defaultValue_ ? null : this.encodeNewValue(); }; /** * A registry setting for string values. * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @constructor * @extends {goog.tweak.BasePrimitiveSetting} * @final */ goog.tweak.StringSetting = function(id, description) { goog.tweak.BasePrimitiveSetting.call(this, id, description, ''); /** * Valid values for the setting. * @type {Array|undefined} */ this.validValues_; }; goog.inherits(goog.tweak.StringSetting, goog.tweak.BasePrimitiveSetting); /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.StringSetting.prototype.logger = goog.log.getLogger('goog.tweak.StringSetting'); /** * @override * @return {string} The tweaks's value. */ goog.tweak.StringSetting.prototype.getValue; /** * @override * @return {string} The tweaks's new value. */ goog.tweak.StringSetting.prototype.getNewValue; /** * @override * @param {string} value The tweaks's value. */ goog.tweak.StringSetting.prototype.setValue; /** * @override * @param {string} value The default value. */ goog.tweak.StringSetting.prototype.setDefaultValue; /** * @override * @return {string} The default value. */ goog.tweak.StringSetting.prototype.getDefaultValue; /** * @override */ goog.tweak.StringSetting.prototype.encodeNewValue = function() { return this.getNewValue(); }; /** * Sets the valid values for the setting. * @param {Array|undefined} values Valid values. */ goog.tweak.StringSetting.prototype.setValidValues = function(values) { this.assertNotInitialized('setValidValues'); this.validValues_ = values; // Set the default value to the first value in the list if the current // default value is not within it. if (values && !goog.array.contains(values, this.getDefaultValue())) { this.setDefaultValue(values[0]); } }; /** * Returns the valid values for the setting. * @return {Array|undefined} Valid values. */ goog.tweak.StringSetting.prototype.getValidValues = function() { return this.validValues_; }; /** * @override */ goog.tweak.StringSetting.prototype.initialize = function(value) { if (value == null) { this.setValue(this.getDefaultValue()); } else { var validValues = this.validValues_; if (validValues) { // Make the query parameter values case-insensitive since users might // type them by hand. Make the capitalization that is actual used come // from the list of valid values. value = value.toLowerCase(); for (var i = 0, il = validValues.length; i < il; ++i) { if (value == validValues[i].toLowerCase()) { this.setValue(validValues[i]); return; } } // Warn if the value is not in the list of allowed values. goog.log.warning( this.logger, 'Tweak ' + this.getId() + ' has value outside of expected range:' + value); } this.setValue(value); } }; /** * A registry setting for numeric values. * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @constructor * @extends {goog.tweak.BasePrimitiveSetting} * @final */ goog.tweak.NumericSetting = function(id, description) { goog.tweak.BasePrimitiveSetting.call(this, id, description, 0); /** * Valid values for the setting. * @type {Array|undefined} */ this.validValues_; }; goog.inherits(goog.tweak.NumericSetting, goog.tweak.BasePrimitiveSetting); /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.NumericSetting.prototype.logger = goog.log.getLogger('goog.tweak.NumericSetting'); /** * @override * @return {number} The tweaks's value. */ goog.tweak.NumericSetting.prototype.getValue; /** * @override * @return {number} The tweaks's new value. */ goog.tweak.NumericSetting.prototype.getNewValue; /** * @override * @param {number} value The tweaks's value. */ goog.tweak.NumericSetting.prototype.setValue; /** * @override * @param {number} value The default value. */ goog.tweak.NumericSetting.prototype.setDefaultValue; /** * @override * @return {number} The default value. */ goog.tweak.NumericSetting.prototype.getDefaultValue; /** * @override */ goog.tweak.NumericSetting.prototype.encodeNewValue = function() { return '' + this.getNewValue(); }; /** * Sets the valid values for the setting. * @param {Array|undefined} values Valid values. */ goog.tweak.NumericSetting.prototype.setValidValues = function(values) { this.assertNotInitialized('setValidValues'); this.validValues_ = values; // Set the default value to the first value in the list if the current // default value is not within it. if (values && !goog.array.contains(values, this.getDefaultValue())) { this.setDefaultValue(values[0]); } }; /** * Returns the valid values for the setting. * @return {Array|undefined} Valid values. */ goog.tweak.NumericSetting.prototype.getValidValues = function() { return this.validValues_; }; /** * @override */ goog.tweak.NumericSetting.prototype.initialize = function(value) { if (value == null) { this.setValue(this.getDefaultValue()); } else { var coercedValue = +value; // Warn if the value is not in the list of allowed values. if (this.validValues_ && !goog.array.contains(this.validValues_, coercedValue)) { goog.log.warning( this.logger, 'Tweak ' + this.getId() + ' has value outside of expected range: ' + value); } if (isNaN(coercedValue)) { goog.log.warning( this.logger, 'Tweak ' + this.getId() + ' has value of NaN, resetting to ' + this.getDefaultValue()); this.setValue(this.getDefaultValue()); } else { this.setValue(coercedValue); } } }; /** * A registry setting that can be either true of false. * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @constructor * @extends {goog.tweak.BasePrimitiveSetting} */ goog.tweak.BooleanSetting = function(id, description) { goog.tweak.BasePrimitiveSetting.call(this, id, description, false); }; goog.inherits(goog.tweak.BooleanSetting, goog.tweak.BasePrimitiveSetting); /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.BooleanSetting.prototype.logger = goog.log.getLogger('goog.tweak.BooleanSetting'); /** * @override * @return {boolean} The tweaks's value. */ goog.tweak.BooleanSetting.prototype.getValue; /** * @override * @return {boolean} The tweaks's new value. */ goog.tweak.BooleanSetting.prototype.getNewValue; /** * @override * @param {boolean} value The tweaks's value. */ goog.tweak.BooleanSetting.prototype.setValue; /** * @override * @param {boolean} value The default value. */ goog.tweak.BooleanSetting.prototype.setDefaultValue; /** * @override * @return {boolean} The default value. */ goog.tweak.BooleanSetting.prototype.getDefaultValue; /** * @override */ goog.tweak.BooleanSetting.prototype.encodeNewValue = function() { return this.getNewValue() ? '1' : '0'; }; /** * @override */ goog.tweak.BooleanSetting.prototype.initialize = function(value) { if (value == null) { this.setValue(this.getDefaultValue()); } else { value = value.toLowerCase(); this.setValue(value == 'true' || value == '1'); } }; /** * An entry in a BooleanGroup. * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @param {!goog.tweak.BooleanGroup} group The group that this entry belongs * to. * @constructor * @extends {goog.tweak.BooleanSetting} * @final */ goog.tweak.BooleanInGroupSetting = function(id, description, group) { goog.tweak.BooleanSetting.call(this, id, description); /** * The token to use in the query parameter. * @type {string} * @private */ this.token_ = this.getId().toLowerCase(); /** * The BooleanGroup that this setting belongs to. * @type {!goog.tweak.BooleanGroup} * @private */ this.group_ = group; // Take setting out of top-level query parameter list. goog.tweak.BooleanInGroupSetting.superClass_.setParamName.call(this, null); }; goog.inherits(goog.tweak.BooleanInGroupSetting, goog.tweak.BooleanSetting); /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.BooleanInGroupSetting.prototype.logger = goog.log.getLogger('goog.tweak.BooleanInGroupSetting'); /** * @override */ goog.tweak.BooleanInGroupSetting.prototype.setParamName = function(value) { goog.asserts.fail('Use setToken() for BooleanInGroupSetting.'); }; /** * Sets the token to use in the query parameter. * @param {string} value The value. */ goog.tweak.BooleanInGroupSetting.prototype.setToken = function(value) { this.token_ = value; }; /** * Returns the token to use in the query parameter. * @return {string} The value. */ goog.tweak.BooleanInGroupSetting.prototype.getToken = function() { return this.token_; }; /** * Returns the BooleanGroup that this setting belongs to. * @return {!goog.tweak.BooleanGroup} The BooleanGroup that this setting * belongs to. */ goog.tweak.BooleanInGroupSetting.prototype.getGroup = function() { return this.group_; }; /** * A registry setting that contains a group of boolean subfield, where all * entries modify the same query parameter. For example: * ?foo=setting1,-setting2 * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @constructor * @extends {goog.tweak.BaseSetting} * @final */ goog.tweak.BooleanGroup = function(id, description) { goog.tweak.BaseSetting.call(this, id, description); /** * A map of token->child entry. * @type {!Object} * @private */ this.entriesByToken_ = {}; /** * A map of token->true/false for all tokens that appeared in the query * parameter. * @type {!Object} * @private */ this.queryParamValues_ = {}; }; goog.inherits(goog.tweak.BooleanGroup, goog.tweak.BaseSetting); /** * The logger for this class. * @type {goog.log.Logger} * @protected * @override */ goog.tweak.BooleanGroup.prototype.logger = goog.log.getLogger('goog.tweak.BooleanGroup'); /** * Returns the map of token->boolean settings. * @return {!Object} The child settings. */ goog.tweak.BooleanGroup.prototype.getChildEntries = function() { return this.entriesByToken_; }; /** * Adds the given BooleanSetting to the group. * @param {goog.tweak.BooleanInGroupSetting} boolEntry The entry. */ goog.tweak.BooleanGroup.prototype.addChild = function(boolEntry) { this.ensureInitialized(); var token = boolEntry.getToken(); var lcToken = token.toLowerCase(); goog.asserts.assert( !this.entriesByToken_[lcToken], 'Multiple bools registered with token "%s" in group: %s', token, this.getId()); this.entriesByToken_[lcToken] = boolEntry; // Initialize from query param. var value = this.queryParamValues_[lcToken]; if (value != undefined) { boolEntry.initialQueryParamValue = value ? '1' : '0'; } }; /** * @override */ goog.tweak.BooleanGroup.prototype.initialize = function(value) { var queryParamValues = {}; if (value) { var tokens = value.split(/\s*,\s*/); for (var i = 0; i < tokens.length; ++i) { var token = tokens[i].toLowerCase(); var negative = token.charAt(0) == '-'; if (negative) { token = token.substr(1); } queryParamValues[token] = !negative; } } this.queryParamValues_ = queryParamValues; }; /** * @override */ goog.tweak.BooleanGroup.prototype.getNewValueEncoded = function() { this.ensureInitialized(); var nonDefaultValues = []; // Sort the keys so that the generate value is stable. var keys = goog.object.getKeys(this.entriesByToken_); keys.sort(); for (var i = 0, entry; entry = this.entriesByToken_[keys[i]]; ++i) { var encodedValue = entry.getNewValueEncoded(); if (encodedValue != null) { nonDefaultValues.push( (entry.getNewValue() ? '' : '-') + entry.getToken()); } } return nonDefaultValues.length ? nonDefaultValues.join(',') : null; }; /** * A registry action (a button). * @param {string} id The ID for the setting. * @param {string} description A description of what the setting does. * @param {!Function} callback Function to call when the button is clicked. * @constructor * @extends {goog.tweak.BaseEntry} * @final */ goog.tweak.ButtonAction = function(id, description, callback) { goog.tweak.BaseEntry.call(this, id, description); this.addCallback(callback); this.setRestartRequired(false); }; goog.inherits(goog.tweak.ButtonAction, goog.tweak.BaseEntry);