Nobody likes boring code
Mozilla products like Thunderbird and Firefox represent setting choices in a textual “tree” system. (Read the intro.) Preferences that begin with print. live on the tree branch dealing with printing while those that begin with app.update. are on the auto-update system’s branch. An easy concept to grasp. In practice, however, the simplicity will easily become a drone of defalutPref(“app.update.auto”, false); if you are not careful.
// This is a boring autoconfig script defaultPref("browser.dom.window.dump.enabled", true); defaultPref("browser.download.manager.retention", 0); defaultPref("browser.download.manager.showAlertOnComplete", false); defaultPref("browser.download.manager.showWhenStarting", false); defaultPref("browser.download.save_converter_index", 0); defaultPref("browser.feeds.handler", "reader"); defaultPref("browser.feeds.handler.default", "web"); defaultPref("browser.feeds.showFirstRunUI", false); defaultPref("browser.history_expire_days.mirror", 180); // and so on.... |
Working at The Preference Factory
Instead of hand-cranking preferences I developed an object prototype to mechanize large blocks of related settings. The resulting code is more readable and easier to maintain when it looks something like this:
// Some hypothetical printing preferences var printPrefs = new PreferenceFactory("print"); // Lock out printing background or in color and don't allow font download printPrefs.setPrefs( { print_bgcolor: false, print_bgimages: false, print_downoadfonts: false, print_in_color: false }, "lock" ); // Make some sensible defaults printPrefs.setPrefs( { print_orientation: 0, // Letter print_to_file: false } ); |
Whitespace focuses your attention on the block and the object eliminates monotonous pref-branch statement repetition. For the experienced programmer this approach is the familiar object-oriented one of sending messages to receivers. One goal behind this prototype was to help create elegant code, which can be difficult to do in javascript. There is another purpose to PreferenceFactory
which I will cover in a later post.
The Bottling Line
The heart of PreferenceFactory
is a wrapper around the autoconfig API. It performs lockPref()
, defaultPref()
, pref()
and getPref()
on a branch of the preference tree that you specify when creating the object. Here is the engine driving the factory:
function PreferenceFactory(arg) { this.prefNodes = []; // Nodes in the preference tree this.prefBranch = ""; // String representation of the branch if(arg) this.addPrefBranch(arg); // Add nodes to the tree and update the string representation } /* Preference setting helper function myServer.setPref("type", "imap", "lock"); Arguments key: The preference string to set value: Value to assign to key lockLevel: Locking level. Valid values are "default", "lock" and "pref" Default level is "defulat" */ PreferenceFactory.prototype.setPref = function(key,value,lockLevel) { if( lockLevel ) { switch( lockLevel.toLowerCase() ) { case "default": this.defaultPref(key,value); break; case "lock": this.lockPref(key,value); break; case "pref": this.pref(key,value); break; default: throw("Unrecognized locking level: " + lockLevel ); } } else { this.defaultPref(key,value); } } PreferenceFactory.prototype.defaultPref = function(key,value) { if( key ) defaultPref( this.prefBranch + "." + key, value ); } PreferenceFactory.prototype.getPref = function(key) { if( key ) getPref( this.prefBranch + "." + key ); } PreferenceFactory.prototype.lockPref = function(key,value) { if( key ) lockPref( this.prefBranch + "." + key, value ); } PreferenceFactory.prototype.pref = function(key,value) { if( key ) pref( this.prefBranch + "." + key, value ); } |
The “hypothetical print” code example gets its readability from a final function to set multiple preferences. It uses a function I copied from JsUnit called trueTypeOf()
. Its function is much like javascript’s typeof
, but handles more than just the built-in data types. setPrefs()
accepts an array or “hash” and iterates over its elements, calling the above setPref()
on each 2-tuple.
/* Sets multiple preferences Accepts an array or object "hash" and an optional locking level */ PreferenceFactory.prototype.setPrefs = function(prefs,lockLevel) { switch( trueTypeOf(prefs) ) { case "Object": for( thing in prefs ) { this.setPref(thing, prefs[thing], lockLevel); } break; case "Array": if( prefs.length % 2 != 0 ) { throw("Need an even number of strings to set multiple preferences with an array"); } for( i = 0; i < prefs.length; i = i + 2 ) { this.setPref(prefs[i], prefs[i+1], lockLevel); } break; default: throw("I don't know how to set multiple prefs with a " + trueTypeOf(prefs) ); } } |
Download the full source
Scene from an upcoming post
I alluded to another purpose for this prototype and this is a glimpse of how I use it with Thunderbird in the real world. Stay tuned.
// Create an email account var brewingAccount = AccountManager.newAccount({ isDefault: true, type: "imap" }); // Lock Preferences on the IMAP server brewingAccount.server.setPrefs({ capability: 81, hostname: "mail.example.com", port: 993, realhostname: "mail.example.com", realusername: brewingMail, remember_password: false, socketType: 3, type: "imap", userName: brewingMail }, "lock" ); // Default Preferences brewingAccount.server.setPrefs({ check_new_mail: true, check_time: 10, cleanup_inbox_on_exit: true, delete_model: 1, /* Move to trash */ directory: userInfo.env_home + "/Mail", "directory-rel": "[ProfD]../../Mail", empty_trash_on_exit: false, empty_trash_threshhold: 0, name: "Brewing Mail", login_at_startup: true, using_subscription: false }); // Lock Preferences on the SMTP server brewingAccount.smtpServer.setPrefs({ auth_method: 1, /* User/pass */ port: 465, username: brewingMail }, "lock" ); // Default Preferences brewingAccount.smtpServer.setPrefs({ description: "Brewing SMTP server", hostname: "mail.example.com", try_ssl: 3 }); |
The source
/** By Dean Brundage Originally published here: http://blog.deanandadie.net/2010/05/manufacturing-user-preferences-for-mcd/ Mix-in prototype for other objects that want to set defaultPref, lockPref or just plain old pref(). Use the utility function copyPrototype() to copy this object's prototype functions to other objects. This can also be used as a stand-alone object. Pass the prefix string to the constructor Example [mix-in]: function Mail.Server() { // It's very important to update the object's preference path this.Mail(); this.addPrefBranch("server"); // Continue to define Mail.Server } copyPrototype(Mail.Server, PreferenceFactory); // Define the rest of Mail.Server's prototypes // Then you can set preferences on your Mail.Server object myServer = new Mail.Server(); myServer.setPref("type", "imap", "lock"); // Lock the type of server to IMAP Example [stand-alone]: var prefFact = new PreferenceFactory( ["mail", "accountmanager"] ); prefFact.setPref("localfoldersserver", "server2" ); */ function PreferenceFactory(arg) { this.prefNodes = []; this.prefBranch = ""; if(arg) this.addPrefBranch(arg); } // Add a string or many strings to the preference branch PreferenceFactory.prototype.addPrefBranch = function(nodes) { switch( trueTypeOf(nodes) ) { case "String": this.prefNodes.push(nodes); break; case "Array": for( i = 0; i < nodes.length; i++ ) this.prefNodes.push(nodes[i]); break; default: throw("Don't know how to addPrefBranch for a " + trueTypeOf(nodes)); break; } this.prefBranch = this.prefNodes.join("."); } PreferenceFactory.prototype.defaultPref = function(key,value) { if( key ) defaultPref( this.prefBranch + "." + key, value ); } PreferenceFactory.prototype.getPref = function(key) { if( key ) getPref( this.prefBranch + "." + key ); } PreferenceFactory.prototype.lockPref = function(key,value) { if( key ) lockPref( this.prefBranch + "." + key, value ); } PreferenceFactory.prototype.pref = function(key,value) { if( key ) pref( this.prefBranch + "." + key, value ); } /* Preference setting helper function myServer.setPref("type", "imap", "lock"); Arguments key: The preference string to set value: Value to assign to key lockLevel: Locking level. Valid values are "default", "lock" and "pref" Default level is "defulat" */ PreferenceFactory.prototype.setPref = function(key,value,lockLevel) { if( lockLevel ) { switch(lockLevel.toLowerCase()) { case "default": this.defaultPref(key,value); break; case "lock": this.lockPref(key,value); break; case "pref": this.pref(key,value); break; default: throw("Unrecognized locking level: " + lockLevel ); } } else { this.defaultPref(key,value); } } /* Sets multiple preferences Accepts an array or object "hash" and an optional locking level */ PreferenceFactory.prototype.setPrefs = function(prefs,lockLevel) { switch( trueTypeOf(prefs) ) { case "Object": for( thing in prefs ) { this.setPref(thing, prefs[thing], lockLevel); } break; case "Array": if( prefs.length % 2 != 0 ) { throw("Need an even number of strings to set multiple preferences with an array"); } for( i = 0; i < prefs.length; i = i + 2 ) { this.setPref(prefs[i], prefs[i+1], lockLevel); } break; default: throw("I don't know how to set multiple prefs with a " + trueTypeOf(prefs) ); } } function trueTypeOf(something) { // Borrowed from jsUnitCore.js. Thank you. // http://github.com/pivotal/jsunit/blob/master/app/jsUnitCore.js var result = typeof something; try { switch (result) { case 'string': break; case 'boolean': break; case 'number': break; case 'object': case 'function': switch (something.constructor) { case new String().constructor: result = 'String'; break; case new Boolean().constructor: result = 'Boolean'; break; case new Number().constructor: result = 'Number'; break; case new Array().constructor: result = 'Array'; break; case new RegExp().constructor: result = 'RegExp'; break; case new Date().constructor: result = 'Date'; break; case Function: result = 'Function'; break; default: var m = something.constructor.toString().match(/function\s*([^( ]+)\(/); if (m) result = m[1]; else break; } break; } } finally { result = result.substr(0, 1).toUpperCase() + result.substr(1); return result; } } |