Infrastructure for the preferences system.
At the top, the level at which most people will interact, is the PreferencesSystem
object.
The most common operation is get(id)
, which simply retrieves the value of a given preference.
The PreferencesSystem has a collection of Scopes, which it traverses in a specified order. Each Scope holds one level of settings.
PreferencesManager.js sets up a singleton PreferencesSystem that has the following Scopes:
For example, if spaceUnits has a value set in a .brackets.json file near the open file, then a call to get("spaceUnits") would return the value from that file. File values come first, user values next, default values last. If the setting is not known at all, undefined is returned.
Each Scope has an associated Storage object that knows how to load and save the preferences value for that Scope. There are two implementations: MemoryStorage and FileStorage.
The final concept used is that of Layers, which can be added to Scopes. Generally, a Layer looks for a collection of preferences that are nested in some fashion in the Scope's data. Under certain circumstances (decided upon by the Layer object), those nested preferences will take precedence over the main preferences in the Scope.
Utility for PreferencesSystem & PrefixedPreferencesSystem -- attach EventDispatcher's on()/off() implementation as private _on_internal()/_off_internal() methods, so the custom on()/off() APIs these classes use can leverage EventDispatcher code internally. Also attach the regular public trigger().
function _addEventDispatcherImpl(proto) {
var temp = {};
EventDispatcher.makeEventDispatcher(temp);
proto._on_internal = temp.on;
proto._off_internal = temp.off;
proto.trigger = temp.trigger;
}
function _findMatchingGlob(pathData, filename) {
var globs = Object.keys(pathData),
globCounter;
if (!filename) {
return;
}
for (globCounter = 0; globCounter < globs.length; globCounter++) {
var glob = globs[globCounter];
if (globmatch(filename, glob)) {
return glob;
}
}
}
MemoryStorage, as the name implies, stores the preferences in memory. This is suitable for single session data or testing.
function MemoryStorage(data) {
this.data = data || {};
}
MemoryStorage.prototype = {
MemoryStorage is not stored in a file, so fileChanged is ignored.
fileChanged: function (filePath) {
}
};
// MemoryStorage never actually dispatches change events, but Storage interface requires implementing on()/off()
EventDispatcher.makeEventDispatcher(MemoryStorage.prototype);
Synchronously returns the data stored in this storage. The original object (not a clone) is returned.
load: function () {
var result = new $.Deferred();
result.resolve(this.data);
return result.promise();
},
Synchronously saves the data to this storage. This saves
the newData
object reference without cloning it.
save: function (newData) {
var result = new $.Deferred();
this.data = newData;
result.resolve();
return result.promise();
},
Error type for problems parsing preference files.
function ParsingError(message) {
this.name = "ParsingError";
this.message = message || "";
}
ParsingError.prototype = new Error();
Loads/saves preferences from a JSON file on disk.
function FileStorage(path, createIfMissing, recreateIfInvalid) {
this.path = path;
this.createIfMissing = createIfMissing;
this.recreateIfInvalid = recreateIfInvalid;
this._lineEndings = FileUtils.getPlatformLineEndings();
}
FileStorage.prototype = {
If the filename matches this Storage's path, a changed message is triggered.
fileChanged: function (filePath) {
if (filePath === this.path) {
this.trigger("changed");
}
}
};
EventDispatcher.makeEventDispatcher(FileStorage.prototype);
Loads the preferences from disk. Can throw an exception if the file is not readable or parseable.
load: function () {
var result = new $.Deferred();
var path = this.path;
var createIfMissing = this.createIfMissing;
var recreateIfInvalid = this.recreateIfInvalid;
var self = this;
if (path) {
var prefFile = FileSystem.getFileForPath(path);
prefFile.read({}, function (err, text) {
if (err) {
if (createIfMissing) {
// Unreadable file is also unwritable -- delete so get recreated
if (recreateIfInvalid && (err === FileSystemError.NOT_READABLE || err === FileSystemError.UNSUPPORTED_ENCODING)) {
appshell.fs.moveToTrash(path, function (err) {
if (err) {
console.log("Cannot move unreadable preferences file " + path + " to trash!!");
} else {
console.log("Brackets has recreated the unreadable preferences file " + path + ". You may refer to the deleted file in trash in case you need it!!");
}
}.bind(this));
}
result.resolve({});
} else {
result.reject(new Error("Unable to load preferences at " + path + " " + err));
}
return;
}
self._lineEndings = FileUtils.sniffLineEndings(text);
// If the file is empty, turn it into an empty object
if (/^\s*$/.test(text)) {
result.resolve({});
} else {
try {
result.resolve(JSON.parse(text));
} catch (e) {
if (recreateIfInvalid) {
// JSON parsing error -- recreate the preferences file
appshell.fs.moveToTrash(path, function (err) {
if (err) {
console.log("Cannot move unparseable preferences file " + path + " to trash!!");
} else {
console.log("Brackets has recreated the Invalid JSON preferences file " + path + ". You may refer to the deleted file in trash in case you need it!!");
}
}.bind(this));
result.resolve({});
} else {
result.reject(new ParsingError("Invalid JSON settings at " + path + "(" + e.toString() + ")"));
}
}
}
});
} else {
result.resolve({});
}
return result.promise();
},
Saves the new data to disk.
save: function (newData) {
var result = new $.Deferred();
var path = this.path;
var prefFile = FileSystem.getFileForPath(path);
if (path) {
try {
var text = JSON.stringify(newData, null, 4);
// maintain the original line endings
text = FileUtils.translateLineEndings(text, this._lineEndings);
prefFile.write(text, {}, function (err) {
if (err) {
result.reject("Unable to save prefs at " + path + " " + err);
} else {
result.resolve();
}
});
} catch (e) {
result.reject("Unable to convert prefs to JSON" + e.toString());
}
} else {
result.resolve();
}
return result.promise();
},
A Scope
is a data container that is tied to a Storage
.
Additionally, Scope
s support "layers" which are additional levels of preferences
that are stored within a single preferences file.
function Scope(storage) {
this.storage = storage;
storage.on("changed", this.load.bind(this));
this.data = {};
this._dirty = false;
this._layers = [];
this._layerMap = {};
this._exclusions = [];
}
_.extend(Scope.prototype, {
_performSet: function (id, value) {
if (!_.isEqual(this.data[id], value)) {
this._dirty = true;
if (value === undefined) {
delete this.data[id];
} else {
this.data[id] = _.cloneDeep(value);
}
return true;
}
return false;
},
Adds a Layer to this Scope. The Layer object should define a key
, which
represents the subset of the preference data that the Layer works with.
Layers should also define get
and getKeys
operations that are like their
counterparts in Scope but take "data" as the first argument.
Listeners are notified of potential changes in preferences with the addition of this layer.
addLayer: function (layer) {
this._layers.push(layer);
this._layerMap[layer.key] = layer;
this._exclusions.push(layer.key);
this.trigger(PREFERENCE_CHANGE, {
ids: layer.getKeys(this.data[layer.key], {})
});
},
Determines if there are likely to be any changes based on the change of context.
contextChanged: function (oldContext, newContext) {
var changes = [],
data = this.data;
_.each(this._layers, function (layer) {
if (data[layer.key] && oldContext[layer.key] !== newContext[layer.key]) {
var changesInLayer = layer.contextChanged(data[layer.key],
oldContext,
newContext);
if (changesInLayer) {
changes.push(changesInLayer);
}
}
});
return _.union.apply(null, changes);
}
});
EventDispatcher.makeEventDispatcher(Scope.prototype);
// Utility functions for the PathLayer
Tells the Scope that the given file has been changed so that the Storage can be reloaded if needed.
fileChanged: function (filePath) {
this.storage.fileChanged(filePath);
},
Get the value for id, given the context. The context is provided to layers which may override the value from the main data of the Scope. Note that layers will often exclude values from consideration.
get: function (id, context) {
var layerCounter,
layers = this._layers,
layer,
data = this.data,
result;
context = context || {};
for (layerCounter = 0; layerCounter < layers.length; layerCounter++) {
layer = layers[layerCounter];
result = layer.get(data[layer.key], id, context);
if (result !== undefined) {
return result;
}
}
if (this._exclusions.indexOf(id) === -1) {
return data[id];
}
},
Get the preference IDs that are set in this Scope. All layers are added in. If context is not provided, the set of all keys in the Scope including all keys in each layer will be returned.
getKeys: function (context) {
context = context || {};
var layerCounter,
layers = this._layers,
layer,
data = this.data;
var keySets = [_.difference(_.keys(data), this._exclusions)];
for (layerCounter = 0; layerCounter < layers.length; layerCounter++) {
layer = layers[layerCounter];
keySets.push(layer.getKeys(data[layer.key], context));
}
return _.union.apply(null, keySets);
},
Get the location in this Scope (if any) where the given preference is set.
getPreferenceLocation: function (id, context) {
var layerCounter,
layers = this._layers,
layer,
data = this.data,
result;
context = context || {};
for (layerCounter = 0; layerCounter < layers.length; layerCounter++) {
layer = layers[layerCounter];
result = layer.getPreferenceLocation(data[layer.key], id, context);
if (result !== undefined) {
return {
layer: layer.key,
layerID: result
};
}
}
if (this._exclusions.indexOf(id) === -1 && data[id] !== undefined) {
// The value is defined in this Scope, which means we need to return an
// empty object as a signal to the PreferencesSystem that this pref
// is defined in this Scope (in the base data)
return {};
}
// return undefined when this Scope does not have the requested pref
return undefined;
},
Loads the prefs for this Scope
from the Storage
.
load: function () {
var result = new $.Deferred();
this.storage.load()
.then(function (data) {
var oldKeys = this.getKeys();
this.data = data;
result.resolve();
this.trigger(PREFERENCE_CHANGE, {
ids: _.union(this.getKeys(), oldKeys)
});
}.bind(this))
.fail(function (error) {
result.reject(error);
});
return result.promise();
},
Saves the prefs for this Scope
.
save: function () {
var self = this;
if (this._dirty) {
self._dirty = false;
return this.storage.save(this.data);
} else {
return (new $.Deferred()).resolve().promise();
}
},
Sets the value for id
. The value is set at the location given, or at the current
location for the preference if no location is specified. If an invalid location is
given, nothing will be set and no exception is thrown.
set: function (id, value, context, location) {
if (!location) {
location = this.getPreferenceLocation(id, context);
}
if (location && location.layer) {
var layer = this._layerMap[location.layer];
if (layer) {
if (this.data[layer.key] === undefined) {
this.data[layer.key] = {};
}
var wasSet = layer.set(this.data[layer.key], id, value, context, location.layerID);
this._dirty = this._dirty || wasSet;
return wasSet;
} else {
return false;
}
} else {
return this._performSet(id, value);
}
},
Create a default project layer object that has a single property "key" with "project" as its value.
function ProjectLayer() {
this.projectPath = null;
}
ProjectLayer.prototype = {
key: "project",
Retrieve the current value based on the current project path in the layer.
get: function (data, id) {
if (!data || !this.projectPath) {
return;
}
if (data[this.projectPath] && (data[this.projectPath][id] !== undefined)) {
return data[this.projectPath][id];
}
return;
},
Retrieves the keys provided by this layer object.
getKeys: function (data) {
if (!data) {
return;
}
return _.union.apply(null, _.map(_.values(data), _.keys));
},
Gets the location in which the given pref was set, if it was set within this project layer for the current project path.
getPreferenceLocation: function (data, id) {
if (!data || !this.projectPath) {
return;
}
if (data[this.projectPath] && (data[this.projectPath][id] !== undefined)) {
return this.projectPath;
}
return;
},
Sets the preference value in the given data structure for the layerID provided. If no layerID is provided, then the current project path is used. If a layerID is provided and it does not exist, it will be created.
This function returns whether or not a value was set.
set: function (data, id, value, context, layerID) {
if (!layerID) {
layerID = this.getPreferenceLocation(data, id);
}
if (!layerID) {
return false;
}
var section = data[layerID];
if (!section) {
data[layerID] = section = {};
}
if (!_.isEqual(section[id], value)) {
if (value === undefined) {
delete section[id];
} else {
section[id] = _.cloneDeep(value);
}
return true;
}
return false;
},
function LanguageLayer() {
}
LanguageLayer.prototype = {
key: "language",
Determines if there are preference IDs that could change as a result of the context change. This implementation considers only changes in language.
contextChanged: function (data, oldContext, newContext) {
// this function is called only if the language has changed
if (newContext.language === undefined) {
return _.keys(data[oldContext.language]);
}
if (oldContext.language === undefined) {
return _.keys(data[newContext.language]);
}
return _.union(_.keys(data[newContext.language]), _.keys(data[oldContext.language]));
}
};
Retrieve the current value based on the specified context. If the context does contain language field, undefined is returned.
get: function (data, id, context) {
if (!data || !context.language) {
return;
}
if (data[context.language] && (data[context.language][id] !== undefined)) {
return data[context.language][id];
}
return;
},
Retrieves the keys provided by this layer object. If the context is empty, it will return all the keys provided in all the layerIDs (languages).
getKeys: function (data, context) {
if (!data) {
return;
}
// do not upset other layers if context for the this one is not specified
if (!_.isEmpty(context)) {
if (data[context.language]) {
return _.keys(data[context.language]);
} else {
return [];
}
} else {
return _.union.apply(null, _.map(_.values(data), _.keys));
}
},
Gets the location in which the given pref was set, if it was set within this language layer for the current language.
getPreferenceLocation: function (data, id, context) {
if (!data || !context.language) {
return;
}
if (data[context.language] && (data[context.language][id] !== undefined)) {
return context.language;
}
return;
},
Sets the preference value in the given data structure for the layerID provided. If no layerID is provided, then it will be determined using getPreferenceLocation. If a layerID is located, but it does not exist, it will be created.
This function returns whether or not a value was set.
set: function (data, id, value, context, layerID) {
if (!layerID) {
layerID = this.getPreferenceLocation(data, id, context);
}
if (!layerID) {
return false;
}
var section = data[layerID];
if (!section) {
data[layerID] = section = {};
}
if (!_.isEqual(section[id], value)) {
if (value === undefined) {
delete section[id];
if (_.isEmpty(section)) {
delete data[layerID];
}
} else {
section[id] = _.cloneDeep(value);
}
return true;
}
return false;
},
There can be multiple paths and they are each checked in turn. The first that matches the currently edited file wins.
function PathLayer(prefFilePath) {
this.setPrefFilePath(prefFilePath);
}
PathLayer.prototype = {
key: "path",
Determines if there are preference IDs that could change as a result of a change in the context. This implementation considers only the path portion of the context and looks up matching globes if any.
contextChanged: function (data, oldContext, newContext) {
var newGlob = _findMatchingGlob(data,
FileUtils.getRelativeFilename(this.prefFilePath, newContext[this.key])),
oldGlob = _findMatchingGlob(data,
FileUtils.getRelativeFilename(this.prefFilePath, oldContext[this.key]));
if (newGlob === oldGlob) {
return;
}
if (newGlob === undefined) {
return _.keys(data[oldGlob]);
}
if (oldGlob === undefined) {
return _.keys(data[newGlob]);
}
return _.union(_.keys(data[oldGlob]), _.keys(data[newGlob]));
}
};
Retrieve the current value based on the filename in the context object, comparing globs relative to the prefFilePath that this PathLayer was set up with.
get: function (data, id, context) {
var glob = this.getPreferenceLocation(data, id, context);
if (!glob) {
return;
}
return data[glob][id];
},
Retrieves the keys provided by this layer object. If context with a filename is provided, only the keys for the matching file glob are given. Otherwise, all keys for all globs are provided.
getKeys: function (data, context) {
if (!data) {
return;
}
var relativeFilename = FileUtils.getRelativeFilename(this.prefFilePath, context[this.key]);
if (relativeFilename) {
var glob = _findMatchingGlob(data, relativeFilename);
if (glob) {
return _.keys(data[glob]);
} else {
return [];
}
}
return _.union.apply(null, _.map(_.values(data), _.keys));
},
Gets the location in which the given pref was set, if it was set within this path layer for the current path.
getPreferenceLocation: function (data, id, context) {
if (!data) {
return;
}
var relativeFilename = FileUtils.getRelativeFilename(this.prefFilePath, context[this.key]);
if (!relativeFilename) {
return;
}
return _findMatchingGlob(data, relativeFilename);
},
Sets the preference value in the given data structure for the layerID provided. If no layerID is provided, then the current layer is used. If a layerID is provided and it does not exist, it will be created.
This function returns whether or not a value was set.
set: function (data, id, value, context, layerID) {
if (!layerID) {
layerID = this.getPreferenceLocation(data, id, context);
}
if (!layerID) {
return false;
}
var section = data[layerID];
if (!section) {
data[layerID] = section = {};
}
if (!_.isEqual(section[id], value)) {
if (value === undefined) {
delete section[id];
} else {
section[id] = _.cloneDeep(value);
}
return true;
}
return false;
},
Represents a single, known Preference.
function Preference(properties) {
_.extend(this, properties);
}
EventDispatcher.makeEventDispatcher(Preference.prototype);
Provides a subset of the PreferencesSystem functionality with preference access always occurring with the given prefix.
function PrefixedPreferencesSystem(base, prefix) {
this.base = base;
this.prefix = prefix;
this._listenerInstalled = false;
}
PrefixedPreferencesSystem.prototype = {
_installListener: function () {
if (this._listenerInstalled) {
return;
}
var self = this,
prefix = this.prefix;
var onlyWithPrefix = function (id) {
if (_.startsWith(id, prefix)) {
return true;
}
return false;
};
var withoutPrefix = function (id) {
return id.substr(prefix.length);
};
this.base.on(PREFERENCE_CHANGE, function (e, data) {
var prefixedIds = data.ids.filter(onlyWithPrefix);
if (prefixedIds.length > 0) {
self.trigger(PREFERENCE_CHANGE, {
ids: prefixedIds.map(withoutPrefix)
});
}
});
this._listenerInstalled = true;
},
Defines a new (prefixed) preference.
definePreference: function (id, type, initial, options) {
return this.base.definePreference(this.prefix + id, type, initial, options);
},
Gets the prefixed preference
get: function (id, context) {
context = context || {};
return this.base.get(this.prefix + id, this.base._getContext(context));
},
Get the prefixed preference object
getPreference: function (id) {
return this.base.getPreference(this.prefix + id);
},
Gets the location in which the value of a prefixed preference has been set.
getPreferenceLocation: function (id, context) {
return this.base.getPreferenceLocation(this.prefix + id, context);
},
Turns off the event handlers for a given event, optionally for a specific preference or a specific handler function.
off: function (event, preferenceID, handler) {
if (typeof preferenceID === "function") {
handler = preferenceID;
preferenceID = null;
}
if (preferenceID) {
var pref = this.getPreference(preferenceID);
pref.off(event, handler);
} else {
this._off_internal(event, handler);
}
},
Sets up a listener for events for this PrefixedPreferencesSystem. Only prefixed events will notify. Optionally, you can set up a listener for a specific preference.
on: function (event, preferenceID, handler) {
if (typeof preferenceID === "function") {
handler = preferenceID;
preferenceID = null;
}
if (preferenceID) {
var pref = this.getPreference(preferenceID);
pref.on(event, handler);
} else {
this._installListener();
this._on_internal(event, handler);
}
},
Saves the preferences. If a save is already in progress, a Promise is returned for that save operation.
save: function () {
return this.base.save();
}
};
_addEventDispatcherImpl(PrefixedPreferencesSystem.prototype);
Sets the prefixed preference
set: function (id, value, options, doNotSave) {
return this.base.set(this.prefix + id, value, options, doNotSave);
},
PreferencesSystem ties everything together to provide a simple interface for managing the whole prefs system.
It keeps track of multiple Scope levels and also manages path-based Scopes.
It also provides the ability to register preferences, which gives a fine-grained means for listening for changes and will ultimately allow for automatic UI generation.
The contextBuilder is used to construct get/set contexts based on the needs of individual context systems. It can be passed in at construction time or set later.
function PreferencesSystem(contextBuilder) {
this.contextBuilder = contextBuilder;
this._knownPrefs = {};
this._scopes = {
"default": new Scope(new MemoryStorage())
};
this._scopes["default"].load();
this._defaults = {
scopeOrder: ["default"],
_shadowScopeOrder: [{
id: "default",
scope: this._scopes["default"],
promise: (new $.Deferred()).resolve().promise()
}]
};
this._pendingScopes = {};
this._saveInProgress = false;
this._nextSaveDeferred = null;
// The objects that define the different kinds of path-based Scope handlers.
// Examples could include the handler for .brackets.json files or an .editorconfig
// handler.
this._pathScopeDefinitions = {};
// Names of the files that contain path scopes
this._pathScopeFilenames = [];
// Keeps track of cached path scope objects.
this._pathScopes = {};
// Keeps track of change events that need to be sent when change events are resumed
this._changeEventQueue = null;
var notifyPrefChange = function (id) {
var pref = this._knownPrefs[id];
if (pref) {
pref.trigger(PREFERENCE_CHANGE);
}
}.bind(this);
// When we signal a general change message on this manager, we also signal a change
// on the individual preference object.
this.on(PREFERENCE_CHANGE, function (e, data) {
data.ids.forEach(notifyPrefChange);
}.bind(this));
}
_.extend(PreferencesSystem.prototype, {
_addToScopeOrder: function (id, scope, promise, addBefore) {
var shadowScopeOrder = this._defaults._shadowScopeOrder,
shadowEntry,
index,
isPending = false,
self = this;
scope.on(PREFERENCE_CHANGE + ".prefsys", function (e, data) {
self._triggerChange(data);
}.bind(this));
index = _.findIndex(shadowScopeOrder, function (entry) {
return entry.id === id;
});
if (index > -1) {
shadowEntry = shadowScopeOrder[index];
} else {
_getContext: function (context) {
if (context) {
if (this.contextBuilder) {
context = this.contextBuilder(context);
}
if (!context.scopeOrder) {
context.scopeOrder = this._defaults.scopeOrder;
}
return context;
}
return { scopeOrder: this._defaults.scopeOrder };
},
_getScopeOrder: function (context) {
return context.scopeOrder || this._defaults.scopeOrder;
},
_pushToScopeOrder: function (id, before) {
var defaultScopeOrder = this._defaults.scopeOrder,
index = _.findIndex(defaultScopeOrder, function (id) {
return id === before;
});
if (index > -1) {
defaultScopeOrder.splice(index, 0, id);
} else {
// error
throw new Error("Internal error: scope " + before + " should be in the scope order");
}
},
_triggerChange: function (data) {
if (this._changeEventQueue) {
this._changeEventQueue = _.union(this._changeEventQueue, data.ids);
} else {
this.trigger(PREFERENCE_CHANGE, data);
}
},
_tryAddToScopeOrder: function (shadowEntry) {
var shadowScopeOrder = this._defaults._shadowScopeOrder,
index = _.findIndex(shadowScopeOrder, function (entry) {
return entry === shadowEntry;
}),
i = index + 1;
// Find an appropriate scope of lower priority to add it before
while (i < shadowScopeOrder.length) {
if (shadowScopeOrder[i].promise.state() === "pending" ||
shadowScopeOrder[i].promise.state() === "resolved") {
break;
}
i++;
}
switch (shadowScopeOrder[i].promise.state()) {
case "pending":
// cannot decide now, lookup once pending promise is settled
shadowScopeOrder[i].promise.always(function () {
this._tryAddToScopeOrder(shadowEntry);
}.bind(this));
break;
case "resolved":
this._pushToScopeOrder(shadowEntry.id, shadowScopeOrder[i].id);
this.trigger(SCOPEORDER_CHANGE, {
id: shadowEntry.id,
action: "added"
});
this._triggerChange({
ids: shadowEntry.scope.getKeys()
});
break;
default:
throw new Error("Internal error: no scope found to add before. \"default\" is missing?..");
}
},
Adds a new Scope. New Scopes are added at the highest precedence, unless the "before" option is given. The new Scope is automatically loaded.
addScope: function (id, scope, options) {
var promise;
options = options || {};
if (this._scopes[id]) {
throw new Error("Attempt to redefine preferences scope: " + id);
}
// Check to see if scope is a Storage that needs to be wrapped
if (!scope.get) {
scope = new Scope(scope);
}
promise = scope.load();
this._addToScopeOrder(id, scope, promise, options.before);
promise
.fail(function (err) {
// With preferences, it is valid for there to be no file.
// It is not valid to have an unparseable file.
if (err instanceof ParsingError) {
console.error(err);
}
});
return promise;
},
Adds scope to the scope order by its id. The scope should be previously added to the preference system.
addToScopeOrder: function (id, addBefore) {
var shadowScopeOrder = this._defaults._shadowScopeOrder,
index = _.findIndex(shadowScopeOrder, function (entry) {
return entry.id === id;
}),
entry;
if (index > -1) {
entry = shadowScopeOrder[index];
this._addToScopeOrder(entry.id, entry.scope, entry.promise, addBefore);
}
},
Defines a new preference.
definePreference: function (id, type, initial, options) {
options = options || {};
if (this._knownPrefs.hasOwnProperty(id)) {
throw new Error("Preference " + id + " was redefined");
}
var pref = this._knownPrefs[id] = new Preference({
type: type,
initial: initial,
name: options.name,
description: options.description,
validator: options.validator,
excludeFromHints: options.excludeFromHints,
keys: options.keys,
values: options.values,
valueType: options.valueType
});
this.set(id, initial, {
location: {
scope: "default"
}
});
return pref;
},
Tells the PreferencesSystem that the given file has been changed so that any related Scopes can be reloaded.
fileChanged: function (filePath) {
_.forEach(this._scopes, function (scope) {
scope.fileChanged(filePath);
});
},
Get the current value of a preference. The optional context provides a way to change scope ordering or the reference filename for path-based scopes.
get: function (id, context) {
var scopeCounter;
context = this._getContext(context);
var scopeOrder = this._getScopeOrder(context);
for (scopeCounter = 0; scopeCounter < scopeOrder.length; scopeCounter++) {
var scope = this._scopes[scopeOrder[scopeCounter]];
if (scope) {
var result = scope.get(id, context);
if (result !== undefined) {
var pref = this.getPreference(id),
validator = pref && pref.validator;
if (!validator || validator(result)) {
if (pref && pref.type === "object") {
result = _.extend({}, pref.initial, result);
}
return _.cloneDeep(result);
}
}
}
}
},
Returns a clone of all preferences defined.
getAllPreferences: function () {
return _.cloneDeep(this._knownPrefs);
},
Get the preference object for the given ID.
getPreference: function (id) {
return this._knownPrefs[id];
},
Gets the location in which the value of a preference has been set.
getPreferenceLocation: function (id, context) {
var scopeCounter,
scopeName;
context = this._getContext(context);
var scopeOrder = this._getScopeOrder(context);
for (scopeCounter = 0; scopeCounter < scopeOrder.length; scopeCounter++) {
scopeName = scopeOrder[scopeCounter];
var scope = this._scopes[scopeName];
if (scope) {
var result = scope.getPreferenceLocation(id, context);
if (result !== undefined) {
result.scope = scopeName;
return result;
}
}
}
},
Retrieves a PreferencesSystem in which all preference access is prefixed. This helps provide namespacing so that different preferences consumers do not interfere with one another.
The prefix provided has a .
character appended when preference lookups are
done.
getPrefixedSystem: function (prefix) {
return new PrefixedPreferencesSystem(this, prefix + ".");
}
});
_addEventDispatcherImpl(PreferencesSystem.prototype);
// Public interface
exports.PreferencesSystem = PreferencesSystem;
exports.Scope = Scope;
exports.MemoryStorage = MemoryStorage;
exports.PathLayer = PathLayer;
exports.ProjectLayer = ProjectLayer;
exports.LanguageLayer = LanguageLayer;
exports.FileStorage = FileStorage;
});
Turns off the event handlers for a given event, optionally for a specific preference or a specific handler function.
off: function (event, preferenceID, handler) {
if (typeof preferenceID === "function") {
handler = preferenceID;
preferenceID = null;
}
if (preferenceID) {
var pref = this.getPreference(preferenceID);
pref.off(event, handler);
} else {
this._off_internal(event, handler);
}
},
Sets up a listener for events. Optionally, you can set up a listener for a specific preference.
on: function (event, preferenceID, handler) {
if (typeof preferenceID === "function") {
handler = preferenceID;
preferenceID = null;
}
if (preferenceID) {
var pref = this.getPreference(preferenceID);
pref.on(event, handler);
} else {
this._on_internal(event, handler);
}
},
Turns off sending of change events, queueing them up for sending once sending is resumed.
The events are compacted so that each preference that will be notified is only
notified once. (For example, if spaceUnits
is changed 5 times, only one change
event will be sent upon resuming events.)
pauseChangeEvents: function () {
if (!this._changeEventQueue) {
this._changeEventQueue = [];
}
},
Removes a scope from the default scope order.
removeFromScopeOrder: function (id) {
var scope = this._scopes[id];
if (scope) {
_.pull(this._defaults.scopeOrder, id);
scope.off(".prefsys");
this.trigger(SCOPEORDER_CHANGE, {
id: id,
action: "removed"
});
this._triggerChange({
ids: scope.getKeys()
});
}
},
Removes a Scope from this PreferencesSystem. Returns without doing anything if the Scope does not exist. Notifies listeners of preferences that may have changed.
removeScope: function (id) {
var scope = this._scopes[id],
shadowIndex;
if (!scope) {
return;
}
this.removeFromScopeOrder(id);
shadowIndex = _.findIndex(this._defaults._shadowScopeOrder, function (entry) {
return entry.id === id;
});
this._defaults._shadowScopeOrder.splice(shadowIndex, 1);
delete this._scopes[id];
},
Turns sending of events back on, sending any events that were queued while the events were paused.
resumeChangeEvents: function () {
if (this._changeEventQueue) {
this.trigger(PREFERENCE_CHANGE, {
ids: this._changeEventQueue
});
this._changeEventQueue = null;
}
},
Saves the preferences. If a save is already in progress, a Promise is returned for that save operation.
save: function () {
if (this._saveInProgress) {
if (!this._nextSaveDeferred) {
this._nextSaveDeferred = new $.Deferred();
}
return this._nextSaveDeferred.promise();
}
var deferred = this._nextSaveDeferred || (new $.Deferred());
this._saveInProgress = true;
this._nextSaveDeferred = null;
Async.doInParallel(_.values(this._scopes), function (scope) {
if (scope) {
return scope.save();
} else {
return (new $.Deferred()).resolve().promise();
}
}.bind(this))
.then(function () {
this._saveInProgress = false;
if (this._nextSaveDeferred) {
this.save();
}
deferred.resolve();
}.bind(this))
.fail(function (err) {
deferred.reject(err);
});
return deferred.promise();
},
Sets a preference and notifies listeners that there may have been a change. By default, the preference is set in the same location in which it was defined except for the "default" scope. If the current value of the preference comes from the "default" scope, the new value will be set at the level just above default.
set: function (id, value, options, doNotSave) {
options = options || {};
var context = this._getContext(options.context),
// The case where the "default" scope was chosen specifically is special.
// Usually "default" would come up only when a preference did not have any
// user-set value, in which case we'd want to set the value in a different scope.
forceDefault = options.location && options.location.scope === "default" ? true : false,
location = options.location || this.getPreferenceLocation(id, context);
if (!location || (location.scope === "default" && !forceDefault)) {
var scopeOrder = this._getScopeOrder(context);
// The default scope for setting a preference is the lowest priority
// scope after "default".
if (scopeOrder.length > 1) {
location = {
scope: scopeOrder[scopeOrder.length - 2]
};
} else {
return { valid: true, stored: false };
}
}
var scope = this._scopes[location.scope];
if (!scope) {
return { valid: true, stored: false };
}
var pref = this.getPreference(id),
validator = pref && pref.validator;
if (validator && !validator(value)) {
return { valid: false, stored: false };
}
var wasSet = scope.set(id, value, context, location);
if (wasSet) {
if (!doNotSave) {
this.save();
}
this._triggerChange({
ids: [id]
});
}
return { valid: true, stored: wasSet };
},
Signals the context change to all the scopes within the preferences layer. PreferencesManager is in charge of computing the context and signaling the changes to PreferencesSystem.
signalContextChanged: function (oldContext, newContext) {
var changes = [];
_.each(this._scopes, function (scope) {
var changedInScope = scope.contextChanged(oldContext, newContext);
if (changedInScope) {
changes.push(changedInScope);
}
});
changes = _.union.apply(null, changes);
if (changes.length > 0) {
this._triggerChange({
ids: changes
});
}
},