]> git.donarmstrong.com Git - x_full.git/blob - .mozilla/firefox/default/extensions/itsalltext@docwhat.gerf.org/chrome/content/itsalltext.js
set max memory capacity
[x_full.git] / .mozilla / firefox / default / extensions / itsalltext@docwhat.gerf.org / chrome / content / itsalltext.js
1 /*
2  *  It's All Text - Easy external editing of web forms.
3  *  Copyright 2006 Christian Höltje
4  *
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License or
8  *  any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License along
16  *  with this program; if not, write to the Free Software Foundation, Inc.,
17  *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19
20 // @todo [9] IDEA: dropdown list for charsets (utf-8, western-iso, default)?
21 // @todo [3] Have a menu/context menu item for turning on monitoring/watch.
22 // @todo [9] Menu item to pick the file to load into a textarea.
23 // @todo [9] Hot-keys for editing or opening the context menu.
24
25 var ItsAllText = function() {
26     /**
27      * This data is all private, which prevents security problems and it
28      * prevents clutter and collection.
29      * @type Object
30      */
31     var that = this;
32
33     /**
34      * Used for tracking all the all the textareas that we are watching.
35      * @type Hash
36      */
37     that.tracker = {};
38
39     /**
40      * Keeps track of all the refreshes we are running.
41      * @type Array
42      */
43     var cron = [null]; // Eat the 0th position
44
45     /**
46      * A constant, a string used for things like the preferences.
47      * @type String
48      */
49     that.MYSTRING = 'itsalltext';
50
51     /**
52      * A constant, the version number.  Set by the Makefile.
53      * @type String
54      */
55     that.VERSION = '0.7.3';
56
57     /**
58      * A constant, the url to the readme.
59      * @type String
60      */
61     that.README = 'chrome://itsalltext/locale/readme.xhtml';
62
63     /* The XHTML Namespace */
64     that.XHTMLNS = "http://www.w3.org/1999/xhtml";
65
66     /* The XUL Namespace */
67     that.XULNS   = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
68
69
70     var string_bundle = Components.classes["@mozilla.org/intl/stringbundle;1"].
71         getService(Components.interfaces.nsIStringBundleService);
72     /**
73      * A localization bundle.  Use it like so:
74      * ItsAllText.locale.getStringFromName('blah');
75      */
76     that.locale = string_bundle.createBundle("chrome://itsalltext/locale/itsalltext.properties");
77     /**
78      * Formats a locale string, replacing $N with the arguments in arr.
79      * @param {String} name Locale property name
80      * @param {Array} arr Array of strings to replace in the string.
81      * @returns String
82      */
83     that.localeFormat = function(name, arr) {
84         return this.locale.formatStringFromName(name, arr, arr.length);
85     };
86     /**
87      * Returns the locale string matching name.
88      * @param {String} name Locale property name
89      * @returns String
90      */
91     that.localeString = function(name) {
92         return this.locale.GetStringFromName(name);
93     };
94
95     /**
96      * Create an error message from given arguments.
97      * @param {Object} message One or more objects to be made into strings...
98      */
99     that.logString = function() {
100         var args = Array.prototype.slice.apply(arguments,[0]);
101         for (var i=0; i<args.length; i++) {
102             try {
103                 args[i] = args[i].toString();
104             } catch(e) {
105                 Components.utils.reportError(e);
106                 args[i] = 'toStringFailed';
107             }
108         }
109         args.unshift(that.MYSTRING+':');
110         return args.join(' ');
111     };
112
113     /**
114      * This is a handy debug message.  I'll remove it or disable it when
115      * I release this.
116      * @param {Object} message One or more objects can be passed in to display.
117      */
118     that.log = function() {
119         var message = that.logString.apply(that, arguments);
120         var consoleService, e;
121         try {
122             // idiom: Convert arguments to an array for easy handling.
123             consoleService = Components.
124                 classes["@mozilla.org/consoleservice;1"].
125                 getService(Components.interfaces.nsIConsoleService);
126             consoleService.logStringMessage(message);
127         } catch(e) {
128             Components.utils.reportError(message);
129         }
130     };
131
132     /**
133      * Uses log iff debugging is turned on.  Used for messages that need to
134      * globally logged (firebug only logs locally).
135      * @param {Object} message One or more objects can be passed in to display.
136      */
137     that.debuglog = function() {
138         if (that.preferences.debug) {
139             that.log.apply(that,arguments);
140         }
141     };
142
143     /**
144      * Displays debug information, if debugging is turned on.
145      * Requires Firebug.
146      * @param {Object} message One or more objects can be passed in to display.
147      */
148     that.debug = function() {
149         if (that.preferences.debug) {
150             try { Firebug.Console.logFormatted(arguments); } 
151             catch(e) {
152                 that.log.apply(that,arguments);
153             }
154         }
155     };
156
157     /**
158      * A factory method to make an nsILocalFile object.
159      * @param {String} path A path to initialize the object with (optional).
160      * @returns {nsILocalFile}
161      */
162     that.factoryFile = function(path) {
163         var file = Components.
164             classes["@mozilla.org/file/local;1"].
165             createInstance(Components.interfaces.nsILocalFile);
166         if (typeof(path) == 'string' && path !== '') {
167             file.initWithPath(path);
168         }
169         return file;
170     };
171
172     /**
173      * Returns the directory where we put files to edit.
174      * @returns nsILocalFile The location where we should write editable files.
175      */
176     that.getEditDir = function() {
177         /* Where is the directory that we use. */
178         var fobj = Components.classes["@mozilla.org/file/directory_service;1"].
179             getService(Components.interfaces.nsIProperties).
180             get("ProfD", Components.interfaces.nsIFile);
181         fobj.append(that.MYSTRING);
182         if (!fobj.exists()) {
183             fobj.create(Components.interfaces.nsIFile.DIRECTORY_TYPE,
184                         parseInt('0700',8));
185         }
186         if (!fobj.isDirectory()) {
187             that.error(that.localeFormat('problem_making_directory', [fobj.path]));
188         }
189         return fobj;
190     };
191
192     /**
193      * Cleans out the edit directory, deleting all old files.
194      */
195     that.cleanEditDir = function(force) {
196         force = (force && typeof(force) != 'undefined');
197         var last_week = Date.now() - (1000*60*60*24*7);
198         var fobj = that.getEditDir();
199         var entries = fobj.directoryEntries;
200         var entry;
201         while (entries.hasMoreElements()) {
202             entry = entries.getNext();
203             entry.QueryInterface(Components.interfaces.nsIFile);
204             if(force || !entry.exists() || entry.lastModifiedTime < last_week){
205                 try{
206                     entry.remove(false);
207                 } catch(e) {
208                     that.debug('unable to remove',entry,'because:',e);
209                 }
210             }
211         }
212     };
213
214     /* Clean the edit directory whenever we create a new window. */
215     that.cleanEditDir();
216
217     /* Load the various bits needed to make this work. */
218     var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader);
219     loader.loadSubScript('chrome://itsalltext/content/Color.js', that);
220     loader.loadSubScript('chrome://itsalltext/content/cacheobj.js', that);
221
222     /**
223      * Dictionary for storing the preferences in.
224      * @type Hash
225      */
226     that.preferences = {
227         debug: true,
228
229         /**
230          * Fetches the current value of the preference.
231          * @private
232          * @param {String} aData The name of the pref to fetch.
233          * @returns {Object} The value of the preference.
234          */
235         _get: function(aData) {
236             var po = that.preference_observer;
237             return po._branch['get'+(po.types[aData])+'Pref'](aData);
238         },
239
240         /**
241          * Sets the current preference.
242          * @param {String} aData The name of the pref to change.
243          * @param {Object} value The value to set.
244          */
245         _set: function(aData, value) {
246             var po = that.preference_observer;
247             return po._branch['set'+(po.types[aData])+'Pref'](aData, value);
248         }
249     };
250
251     /**
252      * A Preference Observer.
253      */
254     that.preference_observer = {
255         /**
256          * Dictionary of types (well, really the method needed to get/set the
257          * type.
258          * @type Hash
259          */
260         types: {
261             charset:            'Char',
262             editor:             'Char',
263             refresh:            'Int',
264             debug:              'Bool',
265             disable_gumdrops:   'Bool',
266             extensions:         'Char'
267         },
268
269         /**
270          * Register the observer.
271          */
272         register: function() {
273             var prefService = Components.
274                 classes["@mozilla.org/preferences-service;1"].
275                 getService(Components.interfaces.nsIPrefService);
276             this._branch = prefService.getBranch("extensions."+that.MYSTRING+".");
277             this._branch.QueryInterface(Components.interfaces.nsIPrefBranch2);
278             this._branch.addObserver("", this, false);
279             /* setup the preferences */
280             for(var type in this.types) {
281                 that.preferences[type] = that.preferences._get(type);
282             }
283         },
284
285         /**
286          * Unregister the observer. Not currently used, but may be
287          * useful in the future.
288          */
289         unregister: function() {
290             if (!this._branch) {return;}
291             this._branch.removeObserver("", this);
292         },
293
294         /**
295          * Observation callback.
296          * @param {String} aSubject The nsIPrefBranch we're observing (after appropriate QI)e
297          * @param {String} aData The name of the pref that's been changed (relative to the aSubject).
298          * @param {String} aTopic The string defined by NS_PREFBRANCH_PREFCHANGE_TOPIC_ID
299          */
300         observe: function(aSubject, aTopic, aData) {
301             if (aTopic != "nsPref:changed") {return;}
302             if (that.preferences) {
303                 that.preferences[aData] = that.preferences._get(aData);
304                 if (aData == 'refresh') {
305                     that.monitor.restart();
306                 }
307             }
308         }        
309     };
310
311     /**
312      * A Preference Option: What character set should the file use?
313      * @returns {String} the charset to be used.
314      */
315     that.getCharset = function() {
316         return that.preferences.charset;
317     };
318
319     /**
320      * A Preference Option: How often should we search for new content?
321      * @returns {int} The number of seconds between checking for new content.
322      */
323     that.getRefresh = function() {
324         var refresh = that.preferences.refresh;
325         if (!refresh || refresh < 1) {
326             that.debug('Invalid refresh gotten:',refresh);
327             refresh = 1;
328         }
329         var retval = 1000*refresh;
330         return retval;
331
332     };
333
334     /**
335      * Returns true if the system is running Mac OS X.
336      * @returns {boolean} Is this a Mac OS X system?
337      */
338     that.isDarwin = function() {
339         /* more help:
340          http://developer.mozilla.org/en/docs/Code_snippets:Miscellaneous#Operating_system_detection
341         */
342
343         var is_darwin = that._is_darwin;
344         if (typeof(is_darwin) == 'undefined') {
345             is_darwin = /^Darwin/i.test(Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULRuntime).OS);
346             that._is_darwin = is_darwin;
347         }
348         return is_darwin;
349     };
350
351     /**
352      * A Preference Option: What editor should we use?
353      *
354      * Note: On some platforms, this can return an 
355      * NS_ERROR_FILE_INVALID_PATH exception and possibly others.
356      *
357      * For a complete list of exceptions, see:
358      * http://lxr.mozilla.org/seamonkey/source/xpcom/base/nsError.h#262
359      * @returns {nsILocalFile} A file object of the editor.
360      */
361     that.getEditor = function() {
362         var editor = that.preferences.editor;
363         var retval = null;
364
365         if (editor === '' && that.isDarwin()) {
366             editor = '/usr/bin/open'; 
367             that.preferences._set('editor', editor);
368         }
369
370         if (editor !== '') {
371             retval = that.factoryFile(editor);
372         }
373         return retval;
374     };
375
376     /**
377      * A Preference Option: should we display debugging info?
378      * @returns {bool}
379      */
380     that.getDebug = function() {
381         return that.preferences.debug;
382     };
383
384     /**
385      * A Preference Option: Are the edit gumdrops disabled?
386      * @returns {bool}
387      */
388     that.getDisableGumdrops = function() {
389         return that.preferences.disable_gumdrops;
390     };
391
392     /**
393      * A Preference Option: The list of extensions
394      * @returns Array
395      */
396     that.getExtensions = function() {
397         var string = that.preferences.extensions.replace(/[\n\t ]+/g,'');
398         var extensions = string.split(',');
399         if (extensions.length === 0) {
400             return ['.txt'];
401         } else {
402             return extensions;
403         }
404     };
405     
406     /**
407      * Open the preferences dialog box.
408      * @param{boolean} wait The function won't return until the preference is set.
409      * @private
410      * Borrowed from http://wiki.mozilla.org/XUL:Windows
411      * and utilityOverlay.js's openPreferences()
412      */
413     that.openPreferences = function (wait) {
414         wait = typeof(wait)=='boolean'?wait:false;
415         var paneID = that.MYSTRING + '_preferences';
416         var instantApply = getBoolPref("browser.preferences.instantApply", false) && !wait;
417         var features = "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal");
418
419         var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].getService(Components.interfaces.nsIWindowMediator);
420         var win = wm.getMostRecentWindow("Browser:Preferences");
421         var pane;
422         if (win) {
423             win.focus();
424             if (paneID) {
425                 pane = win.document.getElementById(paneID);
426                 win.document.documentElement.showPane(pane);
427             }
428         } else {
429             openDialog('chrome://itsalltext/chrome/preferences.xul',
430                        "", features, paneID);
431         }
432     };
433
434     /**
435      * A Preference Option: Append an extension
436      * @returns Array
437      */
438     that.appendExtensions = function(ext) {
439         ext = ext.replace(/[\n\t ]+/g,'');
440         var current = that.getExtensions();
441         for(var i=0; i<current.length; i++) {
442             if(ext == current[i]) {
443                 return; // Don't add a duplicate.
444             }
445         }
446         
447         var value = that.preferences.extensions;
448         if(value.replace(/[\t\n ]+/g) === '') {
449             value = ext;
450         } else {
451             value = [value,',',ext].join('');
452         }
453         that.preferences._set('extensions', value);
454     };
455
456     // @todo [3] Profiling and optimization.
457     
458     /**
459      * Returns a cache object
460      * Note: These UIDs are only unique for Its All Text.
461      * @param {Object} node A dom object node or ID to one.
462      * @returns {String} the UID or null.
463      */
464     that.getCacheObj = function(node) {
465         var cobj = null;
466         var str = that.MYSTRING+"_UID";
467         if (typeof(node) == 'string') {
468             cobj = that.tracker[node];
469         } else {
470             if (node && node.hasAttribute(str)) {
471                 cobj = that.tracker[node.getAttribute(str)];
472             }
473             if (!cobj) {
474                 cobj = new ItsAllText.CacheObj(node);
475             }
476         }
477         return cobj;
478     };
479
480     /**
481      * Cleans out all old cache objects.
482      */
483     that.cleanCacheObjs = function() {
484         var count = 0;
485         var cobj, id;
486         for(id in that.tracker) {
487             cobj = that.tracker[id];
488             if (cobj.node.ownerDocument.location === null) {
489                 that.debug('cleaning %s', id);
490                 delete cobj.node;
491                 delete cobj.button;
492                 delete that.tracker[id];
493             } else {
494                 count += 1;
495             }
496         }
497         that.debuglog('tracker count:', count);
498     };
499
500     /**
501      * Refresh Textarea.
502      * @param {Object} node A specific textarea dom object to update.
503      */
504     that.refreshTextarea = function(node, is_chrome) {
505         var cobj = ItsAllText.getCacheObj(node);
506         if(!cobj) { return; }
507
508         cobj.update();
509         if (!is_chrome) { cobj.addGumDrop(); }
510     };
511
512     // @todo [5] Refresh textarea on editor quit.
513     // @todo [9] IDEA: support for input elements as well?
514
515     /**
516      * Refresh Document.
517      * @param {Object} doc The document to refresh.
518      */
519     that.refreshDocument = function(doc) {
520         if(!doc.location) { return; } // it's being cached, but not shown.
521         var is_chrome = (doc.location.protocol == 'chrome:' &&
522                          doc.location.href != that.README);
523         var nodes = doc.getElementsByTagName('textarea');
524         var i;
525         for(i=0; i < nodes.length; i++) {
526             that.refreshTextarea(nodes[i], is_chrome);
527         }
528         nodes = doc.getElementsByTagName('textbox');
529         for(i=0; i < nodes.length; i++) {
530             that.refreshTextarea(nodes[i], is_chrome);
531         }
532     };
533
534     /**
535      * Returns the offset from the containing block.
536      * @param {Object} node A DOM element.
537      * @param {Object} container If unset, then this will use the offsetParent of node. Pass in null to go all the way to the root.
538      * @return {Array} The X & Y page offsets
539      */
540     that.getContainingBlockOffset = function(node, container) {
541         if(typeof(container) == 'undefined') {
542             container = node.offsetParent;
543         }
544         var pos = [node.offsetLeft, node.offsetTop];
545         var pnode = node.offsetParent;
546         while(pnode && (container === null || pnode != container)) {
547             pos[0] += pnode.offsetLeft || 0;
548             pos[1] += pnode.offsetTop  || 0;
549             pos[0] -= pnode.scrollLeft || 0;
550             pos[1] -= pnode.scrollTop  || 0;
551             pnode = pnode.offsetParent;
552         }
553         return pos;
554     };
555
556
557     /**
558      * This function is called regularly to watch changes to web documents.
559      */
560     that.monitor = {
561         id: null,
562         last_now:0,
563         documents: [],
564         /**
565          * Starts or restarts the document monitor.
566          */
567         restart: function() {
568             var rate = that.getRefresh();
569             var id   = that.monitor.id;
570             if (id) {
571                 clearInterval(id);
572             }
573             that.monitor.id = setInterval(that.monitor.watcher, rate);
574         },
575         /**
576          * watches the document 'doc'.
577          * @param {Object} doc The document to watch.
578          */
579         watch: function(doc, force) {
580             var contentType, location, is_html, is_usable, is_my_readme;
581             if (!force) {
582                 /* Check that this is a document we want to play with. */
583                 contentType = doc.contentType;
584                 location = doc.location;
585                 is_html = (contentType=='text/html' ||
586                            contentType=='text/xhtml' ||
587                            contentType=='application/xhtml+xml');
588                 //var is_xul=(contentType=='application/vnd.mozilla.xul+xml');
589                 is_usable = (is_html) && 
590                     location.protocol != 'about:' &&
591                     location.protocol != 'chrome:';
592                 is_my_readme = location.href == that.README;
593                 if (!(is_usable || is_my_readme)) { 
594                     that.debuglog('watch(): ignoring -- ',
595                                   location, contentType);
596                     return;
597                 }
598             }
599
600             that.refreshDocument(doc);
601             that.monitor.documents.push(doc);
602         },
603         /**
604          * Callback to be used by restart()
605          * @private
606          */
607         watcher: function(offset) {
608             var monitor = that.monitor;
609             var rate = that.getRefresh();
610             
611             var now = Date.now();
612             if (now - monitor.last_now < Math.round(rate * 0.9)) {
613                 that.debuglog('monitor.watcher(',offset,') -- skipping catchup refresh');
614                 return;
615             }
616             monitor.last_now = now;
617
618             /* Walk the documents looking for changes */
619             var documents = monitor.documents;
620             that.debuglog('monitor.watcher(',offset,'): ', documents.length);
621             var i, doc;
622             var did_delete = false;
623             for(i in documents) {
624                 doc = documents[i];
625                 if (doc.location) {
626                     that.debuglog('refreshing', doc.location);
627                     that.refreshDocument(doc);
628                 }
629             }
630         },
631         /**
632          * Stops watching doc.
633          * @param {Object} doc The document to watch.
634          */
635         unwatch: function(doc) {
636             var documents = that.monitor.documents;
637             var i;
638             for(i in documents) {
639                 if (documents[i] === doc) {
640                     that.debug('unwatching', doc);
641                     delete documents[i];
642                 }
643             }
644             that.cleanCacheObjs();
645             for(i=documents.length - 1; i >= 0; i--) {
646                 if(typeof(documents[i]) == 'undefined') {
647                     documents.splice(i,1);
648                 }
649             }
650         }
651     };
652
653     /**
654      * Callback whenever the DOM content in a window or tab is loaded.
655      * @param {Object} event An event passed in.
656      */
657     that.onDOMContentLoad = function(event) {
658         if (event.originalTarget.nodeName != "#document") { return; }
659         var doc = event.originalTarget || document;
660         that.monitor.watch(doc);
661         return;
662     };
663
664     /**
665      * Open the editor for a selected node.
666      * @param {Object} node The textarea to get.
667      */
668     that.onEditNode = function(node) {
669         var cobj = that.getCacheObj(node);
670         if(cobj) {
671             cobj.edit();
672         }
673         return;
674     };
675
676     /**
677      * Triggered when the context menu is shown.
678      * @param {Object} event The event passed in by the event handler.
679      */
680     that.onContextMenu = function(event) {
681         var tid, node, tag, is_disabled, cobj, menu;
682         if(event.target) {
683             tid = event.target.id;
684             if (tid == "itsalltext-context-popup" ||
685                 tid == "contentAreaContextMenu") {
686                 node = document.popupNode;
687                 tag = node.nodeName.toLowerCase();
688                 is_disabled = (!(tag == 'textarea' || 
689                                      tag == 'textbox') ||
690                                    node.style.display == 'none' ||
691                                    node.getAttribute('readonly') ||
692                                    node.getAttribute('disabled')
693                                    );
694                 if (tid == "itsalltext-context-popup") {
695                     cobj = that.getCacheObj(node);
696                     that.rebuildMenu(cobj.uid,
697                                      'itsalltext-context-popup',
698                                      is_disabled);
699                 } else {
700                     // tid == "contentAreaContextMenu"
701                     menu = document.getElementById("itsalltext-contextmenu");
702                     menu.setAttribute('hidden', is_disabled);
703                 }
704                     
705             }
706         }
707         return true;
708     };
709
710     that.openReadme = function() {
711         browser = getBrowser();
712         browser.selectedTab = browser.addTab(that.README, null);
713     };
714
715     /**
716      * Initialize the module.  Should be called once, when a window is loaded.
717      * @private
718      */
719     var windowload = function(event) {
720         that.debug("startup(): It's All Text! is watching this window...");
721
722         // Start watching the preferences.
723         that.preference_observer.register();
724
725         // Start the monitor
726         that.monitor.restart();
727
728         var appcontent = document.getElementById("appcontent"); // The Browser
729         if (appcontent) {
730             // Normal web-page.
731             appcontent.addEventListener("DOMContentLoaded", that.onDOMContentLoad,
732                                         true);
733         } else {
734             that.onDOMContentLoad(event); 
735         }
736         // Attach the context menu, if we can.
737         var contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
738         if (contentAreaContextMenu) {
739             contentAreaContextMenu.addEventListener("popupshowing",
740                                                     that.onContextMenu, false);
741         }
742     };
743   
744     // Do the startup when things are loaded.
745     window.addEventListener("load", windowload, true);
746     // Do the startup when things are unloaded.
747     window.addEventListener("unload", function(event){that.monitor.unwatch(event.originalTarget||document); that.preference_observer.unregister();}, true);
748
749 };
750
751 /**
752  * The command that is called when picking a new extension.
753  * @param {Event} event
754  */
755 ItsAllText.prototype.menuNewExtEdit = function(event) {
756     var that = this;
757     var uid = this._current_uid;
758     var cobj = that.getCacheObj(uid);
759
760     var params = {out:null};       
761     window.openDialog("chrome://itsalltext/chrome/newextension.xul", "",
762     "chrome, dialog, modal, resizable=yes", params).focus();
763     var ext;
764     if (params.out) {
765         ext = params.out.extension.replace(/[\n\t ]+/g,'');
766         if(params.out.do_save) {
767             that.appendExtensions(ext);
768         }
769         cobj.edit(ext);
770     }
771 };
772
773 /**
774  * The command that is called when selecting an existing extension.
775  * @param {Event} event
776  */
777 ItsAllText.prototype.menuExtEdit = function(event) {
778     var that = this;
779     var uid = that._current_uid;
780     var ext = event.target.getAttribute('label');
781     var cobj = that.getCacheObj(uid);
782     cobj.edit(ext);
783 };
784
785 /**
786  * Rebuilds the option menu, to reflect the current list of extensions.
787  * @private
788  * @param {String} uid The UID to show in the option menu.
789  */
790 ItsAllText.prototype.rebuildMenu = function(uid, menu_id, is_disabled) {
791     menu_id = typeof(menu_id) == 'string'?menu_id:'itsalltext-optionmenu';
792     is_disabled = (typeof(is_disabled) == 'undefined'||!is_disabled)?false:(is_disabled&&true);
793     var i;
794     var that = this;
795     var exts = that.getExtensions();
796     var menu = document.getElementById(menu_id);
797     var items = menu.childNodes;
798     var items_length = items.length - 1; /* We ignore the preferences item */
799     var node;
800     that._current_uid = uid;
801     var magic_stop_node = null;
802     var magic_start = null;
803     var magic_stop = null;
804
805     // Find the beginning and end of the magic replacement parts.
806     for(i=0; i<items_length; i++) {
807         node = items[i];
808         if (node.nodeName.toLowerCase() == 'menuseparator') {
809             if(magic_start === null) {
810                 magic_start = i;
811             } else if (magic_stop === null) {
812                 magic_stop = i;
813                 magic_stop_node = node;
814             }
815         } else if (node.nodeName.toLowerCase() == 'menuitem') {
816             node.setAttribute('disabled', is_disabled?'true':'false');
817         }
818     }
819
820     // Remove old magic bits
821     for(i = magic_stop - 1; i > magic_start; i--) {
822         menu.removeChild(items[i]);
823     }
824    
825     // Insert the new magic bits
826     for(i=0; i<exts.length; i++) {
827         node = document.createElementNS(that.XULNS, 'menuitem');
828         node.setAttribute('label', exts[i]);
829         node.addEventListener('command', function(event){return that.menuExtEdit(event);}, false);
830         node.setAttribute('disabled', is_disabled?'true':'false');
831         menu.insertBefore(node, magic_stop_node);
832
833     }
834     return menu;
835 };
836
837 ItsAllText = new ItsAllText();
838