]> git.donarmstrong.com Git - x_full.git/blob - .mozilla/firefox/default/extensions/itsalltext@docwhat.gerf.org/chrome/content/cacheobj.js
1f5a1ce13b54790df09a81639d1b536c48112a0d
[x_full.git] / .mozilla / firefox / default / extensions / itsalltext@docwhat.gerf.org / chrome / content / cacheobj.js
1 /**
2  * A Cache object is used to manage the node and the file behind it.
3  * @constructor
4  * @param {Object} node A DOM Node to watch.
5  */
6 function CacheObj(node) {
7     var that = this;
8
9     /* Gumdrop Image URL */
10     that.gumdrop_url    = 'chrome://itsalltext/locale/gumdrop.png';
11     /* Gumdrop Image Width */
12     that.gumdrop_width  = ItsAllText.localeString('gumdrop.width'); 
13     /* Gumdrop Image Height */
14     that.gumdrop_height = ItsAllText.localeString('gumdrop.height');
15
16     that.timestamp = 0;
17     that.size = 0;
18     that.node = node;
19     that.button = null;
20     that.initial_background = '';
21     that._is_watching = false;
22      
23     that.node_id = that.getNodeIdentifier(node);
24     var doc = node.ownerDocument;
25
26     /* This is a unique identifier for use on the web page to prevent the
27      * web page from knowing what it's connected to.
28      * @type String
29      */
30     that.uid = that.hashString([ doc.location.toString(),
31                                  Math.random(),
32                                  that.node_id ].join(':'));
33     // @todo [security] Add a serial to the uid hash.
34
35     node.setAttribute(ItsAllText.MYSTRING+'_UID', that.uid);
36     ItsAllText.tracker[that.uid] = that;
37     
38     /* Figure out where we will store the file.  While the filename can
39      * change, the directory that the file is stored in should not!
40      */
41     var host = window.escape(doc.location.hostname);
42     var hash = that.hashString([ doc.location.protocol,
43                                  doc.location.port,
44                                  doc.location.search,
45                                  doc.location.pathname,
46                                  that.node_id ].join(':'));
47     that.base_filename = [host, hash.slice(0,10)].join('.');
48     /* The current extension.
49      * @type String
50      */
51     that.extension = null;
52
53     /* Stores an nsILocalFile pointing to the current filename.
54      * @type nsILocalFile
55      */
56     that.file = null;
57
58     /* Set the default extension and create the nsIFile object. */
59     var extension = node.getAttribute('itsalltext-extension');
60     if (typeof(extension) != 'string' || !extension.match(/^[.a-z0-9]+$/i)) {
61         extension = ItsAllText.getExtensions()[0];
62     }
63     that.setExtension(extension);
64
65     that.initFromExistingFile();
66
67     /**
68      * A callback for when the textarea/textbox or button has 
69      * the mouse waved over it.
70      * @param {Event} event The event object.
71      */
72     that.mouseover = function(event) {
73         var style = that.button?that.button.style:null;
74         if (style) {
75             style.setProperty('opacity', '0.7', 'important');
76             ItsAllText.refreshTextarea(that.node);
77         }
78     };
79
80     /**
81      * A callback for when the textarea/textbox or button has 
82      * the mouse waved over it and the moved off.
83      * @param {Event} event The event object.
84      */
85     that.mouseout = function(event) {
86         var style = that.button?that.button.style:null;
87         if (style) {
88             style.setProperty('opacity', '0.1', 'important');
89         }
90     };
91 }
92
93 /**
94  * Set the extension for the file to ext.
95  * @param {String} ext The extension.  Must include the dot.  Example: .txt
96  */
97 CacheObj.prototype.setExtension = function(ext) {
98     if (ext == this.extension) {
99         return; /* It's already set.  No problem. */
100     }
101
102     /* Create the nsIFile object */
103     var file = ItsAllText.factoryFile();
104     file.initWithFile(ItsAllText.getEditDir());
105     file.append([this.base_filename,ext].join(''));
106
107     this.extension = ext;
108     this.file = file;
109 };
110
111 /**
112  * This function looks for an existing file and starts to monitor
113  * if the file exists already.  It also deletes all existing files for
114  * this cache object.
115  */
116 CacheObj.prototype.initFromExistingFile = function() {
117     var base = this.base_filename;
118     var fobj = ItsAllText.getEditDir();
119     var entries = fobj.directoryEntries;
120     var ext = null;
121     var tmpfiles = /(\.bak|.tmp|~)$/;
122     var entry;
123     while (entries.hasMoreElements()) {
124         entry = entries.getNext();
125         entry.QueryInterface(Components.interfaces.nsIFile);
126         if (entry.leafName.indexOf(base) === 0) {
127             // startswith
128             if (ext === null && !entry.leafName.match(tmpfiles)) {
129                 ext = entry.leafName.slice(base.length);
130             }
131             try{
132                 entry.remove(false);
133             } catch(e) {
134                 that.debug('unable to remove',entry,'because:',e);
135             }
136         }
137     }
138     if (ext !== null) {
139         this.setExtension(ext);
140         this._is_watching = true;
141     }
142 };
143
144 /**
145  * Returns a unique identifier for the node, within the document.
146  * @returns {String} the unique identifier.
147  */
148 CacheObj.prototype.getNodeIdentifier = function(node) {
149     var id   = node.getAttribute('id');
150     var name, doc, attr, serial;
151     if (!id) {
152         name = node.getAttribute('name');
153         doc = node.ownerDocument.getElementsByTagName('html')[0];
154         attr = ItsAllText.MYSTRING+'_id_serial';
155         
156         /* Get a serial that's unique to this document */
157         serial = doc.getAttribute(attr);
158         if (serial) { serial = parseInt(serial, 10)+1;
159         } else { serial = 1; }
160         id = [ItsAllText.MYSTRING,'generated_id',name,serial].join('_');
161         doc.setAttribute(attr,serial);
162         node.setAttribute('id',id);
163     }
164     return id;
165 };
166
167 /**
168  * Convert to this object to a useful string.
169  * @returns {String} A string representation of this object.
170  */
171 CacheObj.prototype.toString = function() {
172     return [ "CacheObj",
173              " uid=",this.uid,
174              " timestamp=",this.timestamp,
175              " size=",this.size
176     ].join('');
177 };
178
179 /**
180  * Write out the contents of the node.
181  */
182 CacheObj.prototype.write = function() {
183     var foStream = Components.
184         classes["@mozilla.org/network/file-output-stream;1"].
185         createInstance(Components.interfaces.nsIFileOutputStream);
186              
187     /* write, create, truncate */
188     foStream.init(this.file, 0x02 | 0x08 | 0x20, 
189                   parseInt('0600',8), 0); 
190              
191     /* We convert to charset */
192     var conv = Components.
193         classes["@mozilla.org/intl/scriptableunicodeconverter"].
194         createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
195     conv.charset = ItsAllText.getCharset();
196              
197     var text = conv.ConvertFromUnicode(this.node.value);
198     foStream.write(text, text.length);
199     foStream.close();
200              
201     /* Reset Timestamp and filesize, to prevent a spurious refresh */
202     this.timestamp = this.file.lastModifiedTime;
203     this.size      = this.file.fileSize;
204
205     /* Register the file to be deleted on app exit. */
206     Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"].
207         getService(Components.interfaces.nsPIExternalAppLauncher).
208         deleteTemporaryFileOnExit(this.file);
209              
210     return this.file.path;
211 };
212
213 /**
214  * Fetches the computed CSS attribute for a specific node
215  * @param {DOM} node The DOM node to get the information for.
216  * @param {String} attr The CSS-style attribute to fetch (not DOM name).
217  * @returns attribute
218  */
219 CacheObj.prototype.getStyle = function(node, attr) {
220     var view  = node ? node.ownerDocument.defaultView : null;
221     var style = view.getComputedStyle(node, '');
222     return  style.getPropertyCSSValue(attr).cssText;
223 };
224      
225 // @todo [9] IDEA: Pass in the line number to the editor, arbitrary command?
226 // @todo [9] IDEA: Allow the user to pick an alternative editor?
227 // @todo [9] IDEA: A different editor per extension?
228 /**
229  * Edit a textarea as a file.
230  * @param {String} extension The extension of the file to edit.
231  */
232 CacheObj.prototype.edit = function(extension) {
233     if (typeof(extension) == 'string') {
234         this.setExtension(extension);
235     }
236     var filename = this.write();
237     this.initial_background = this.node.style.backgroundColor;
238     this.initial_color      = this.node.style.color;
239     var program = null; 
240     var process;
241     var args, result, ec, e, params;
242              
243     try {
244         program = ItsAllText.getEditor();
245         // checks
246         if (program === null)        { throw {name:"Editor is not set."}; }
247         if (!program.exists())       { throw {name:"NS_ERROR_FILE_NOT_FOUND"}; }
248         /* Mac check because of 
249          * https://bugzilla.mozilla.org/show_bug.cgi?id=322865 */
250         if (!(ItsAllText.isDarwin() || program.isExecutable())) { 
251             throw {name:"NS_ERROR_FILE_ACCESS_DENIED"}; }
252
253         // create an nsIProcess
254         process = Components.
255             classes["@mozilla.org/process/util;1"].
256             createInstance(Components.interfaces.nsIProcess);
257         process.init(program);
258              
259         // Run the process.
260         // If first param is true, calling thread will be blocked until
261         // called process terminates.
262         // Second and third params are used to pass command-line arguments
263         // to the process.
264         args = [filename];
265         result = {};
266         ec = process.run(false, args, args.length, result);
267         this._is_watching = true;
268     } catch(e) {        
269         params = {out:null,
270                       exists: program ? program.exists() : false,
271                       path: ItsAllText.preferences.editor,
272                       exception: e.name };
273         window.openDialog('chrome://itsalltext/chrome/badeditor.xul',
274                           null,
275                           "chrome,titlebar,toolbar,centerscreen,modal",
276                           params);
277         if(params.out !== null && params.out.do_preferences) {
278             ItsAllText.openPreferences(true);
279             this.edit(extension);
280         }
281     }
282 };
283
284 /**
285  * Delete the file from disk.
286  */
287 CacheObj.prototype.remove = function() {
288     if(this.file.exists()) {
289         try {
290             this.file.remove();
291         } catch(e) {
292             that.debug('remove(',this.file.path,'): ',e);
293             return false;
294         }
295     }
296     return true;
297 };
298
299 /**
300  * Read the file from disk.
301  */
302 CacheObj.prototype.read = function() {
303     /* read file, reset ts & size */
304     var DEFAULT_REPLACEMENT_CHARACTER = 65533;
305     var buffer = [];
306     var fis, istream, str, e;
307          
308     try {
309         fis = Components.
310             classes["@mozilla.org/network/file-input-stream;1"].
311             createInstance(Components.interfaces.nsIFileInputStream);
312         fis.init(this.file, 0x01, parseInt('00400',8), 0); 
313         // MODE_RDONLY | PERM_IRUSR
314              
315         istream = Components.
316             classes["@mozilla.org/intl/converter-input-stream;1"].
317             createInstance(Components.interfaces.nsIConverterInputStream);
318         istream.init(fis, ItsAllText.getCharset(), 4096, DEFAULT_REPLACEMENT_CHARACTER);
319              
320         str = {};
321         while (istream.readString(4096, str) !== 0) {
322             buffer.push(str.value);
323         }
324         
325         istream.close();
326         fis.close();
327              
328         this.timestamp = this.file.lastModifiedTime;
329         this.size      = this.file.fileSize;
330              
331         return buffer.join('');
332     } catch(e) {
333         return null;
334     }
335 };
336
337 /**
338  * Has the file object changed?
339  * @returns {boolean} returns true if the file has changed on disk.
340  */
341  CacheObj.prototype.hasChanged = function() {
342      /* Check exists.  Check ts and size. */
343      if(!this._is_watching ||
344         !this.file.exists() ||
345         !this.file.isReadable() ||
346         (this.file.lastModifiedTime == this.timestamp && 
347          this.file.fileSize         == this.size)) {
348          return false;
349      } else {
350          return true;
351      }
352  };
353
354 /**
355  * Part of the fading technique.
356  * @param {Object} pallet A Color blend pallet object.
357  * @param {int}    step   Size of a step.
358  * @param {delay}  delay  Delay in microseconds.
359  */
360 CacheObj.prototype.fadeStep = function(background_pallet, color_pallet, step, delay) {
361     var that = this;
362     return function() {
363         if (step < background_pallet.length) {
364             that.node.style.backgroundColor = background_pallet[step].hex();
365             that.node.style.color = color_pallet[step].hex();
366             step++;
367             setTimeout(that.fadeStep(background_pallet, color_pallet, step, delay),delay);
368         } else {
369             that.node.style.backgroundColor = that.initial_background;
370             that.node.style.color = that.initial_color;
371         }
372     };
373 };
374
375 /**
376  * Node fade technique.
377  * @param {int} steps  Number of steps in the transition.
378  * @param {int} delay  How long to wait between delay (microseconds).
379  */
380 CacheObj.prototype.fade = function(steps, delay) {
381     var color             = this.getStyle(this.node, 'color');
382     var color_stop        = new ItsAllText.Color(color);
383     var color_start       = new ItsAllText.Color('black');
384     var color_pallet      = color_start.blend(color_stop, steps);
385
386     var background        = this.getStyle(this.node, 'background-color');
387     var background_stop   = new ItsAllText.Color(background);
388     var background_start  = new ItsAllText.Color('yellow');
389     var background_pallet = background_start.blend(background_stop, steps);
390     setTimeout(this.fadeStep(background_pallet, color_pallet, 0, delay), delay);
391 };
392
393 /**
394  * Update the node from the file.
395  * @returns {boolean} Returns true ifthe file changed.
396  */
397 CacheObj.prototype.update = function() {
398     var value;
399     if (this.hasChanged()) {
400         value = this.read();
401         if (value !== null) {
402             this.fade(20, 100);
403             this.node.value = value;
404             return true;
405         }
406     }
407     return false; // If we fall through, we 
408 };
409
410 /**
411  * Add the gumdrop to a textarea.
412  * @param {Object} cache_object The Cache Object that contains the node.
413  */
414 CacheObj.prototype.addGumDrop = function() {
415     var cache_object = this;
416     if (cache_object.button !== null) {
417         cache_object.adjust();
418         return; /*already done*/
419     }
420     if (ItsAllText.getDisableGumdrops()) {
421         return;
422     }
423     ItsAllText.debug('addGumDrop()',cache_object.node_id,cache_object.uid);
424     
425     var node = cache_object.node;
426     var doc = node.ownerDocument;
427     var offsetNode = node;
428     if (!node.parentNode) { return; }
429     
430     var gumdrop = doc.createElementNS(ItsAllText.XHTMLNS, "img");
431     gumdrop.setAttribute('src', this.gumdrop_url);
432     var gid = cache_object.getNodeIdentifier(gumdrop);
433     
434     if (ItsAllText.getDebug()) {
435         gumdrop.setAttribute('title', cache_object.node_id);
436     } else {
437         gumdrop.setAttribute('title', ItsAllText.localeString('program_name'));
438     }
439     cache_object.button = gumdrop; // Store it for easy finding in the future.
440     
441     // Image Attributes
442     gumdrop.style.setProperty('cursor',   'pointer', 'important');
443     gumdrop.style.setProperty('display',  'block', 'important');
444     gumdrop.style.setProperty('position',  'absolute', 'important');
445     gumdrop.style.setProperty('padding',   '0', 'important');
446     gumdrop.style.setProperty('margin',   '0', 'important');
447     gumdrop.style.setProperty('border',    'none', 'important');
448     gumdrop.style.setProperty('zIndex',    '1', 'important'); // we want it just above normal items.
449     
450     gumdrop.style.setProperty('width',  this.gumdrop_width+'px',  'important');
451     gumdrop.style.setProperty('height', this.gumdrop_height+'px', 'important');
452
453     gumdrop.setAttribute(ItsAllText.MYSTRING+'_UID', cache_object.uid);
454
455     var clickfun = function(event) {
456         cache_object.edit();
457         event.stopPropagation();
458         return false;
459     };
460     var contextfun = function(event) {
461         /* This took forever to fix; roughly 80+ man hours were spent
462          * over 5 months trying to make this stupid thing work.
463          * The documentation is completely wrong and useless.
464          *
465          * Excuse me while I scream.
466          *
467          * See Mozilla bugs: 287357, 362403, 279703
468          */
469         var popup = ItsAllText.rebuildMenu(cache_object.uid);
470         document.popupNode = popup;
471         popup.showPopup(popup,
472                         event.screenX, event.screenY,
473                         'context', null, null);
474         event.stopPropagation();
475         return false;
476     };
477     
478     // Click event handler
479     gumdrop.addEventListener("click", clickfun, false);
480     gumdrop.addEventListener("contextmenu", contextfun, false);
481     
482     // Insert it into the document
483     var parent = node.parentNode;
484     var nextSibling = node.nextSibling;
485     
486     if (nextSibling) {
487         parent.insertBefore(gumdrop, nextSibling);
488     } else {
489         parent.appendChild(gumdrop);
490     }
491     
492     // Add mouseovers/outs
493     node.addEventListener("mouseover",    cache_object.mouseover, false);
494     node.addEventListener("mouseout",     cache_object.mouseout, false);
495     gumdrop.addEventListener("mouseover", cache_object.mouseover, false);
496     gumdrop.addEventListener("mouseout",  cache_object.mouseout, false);
497     
498     cache_object.mouseout(null);
499     cache_object.adjust();
500 };
501
502 /**
503  * Updates the position of the gumdrop, incase the textarea shifts around.
504  */
505 CacheObj.prototype.adjust = function() {
506     var gumdrop  = this.button;
507     var el       = this.node;
508     var doc      = el.ownerDocument;
509
510     if (ItsAllText.getDisableGumdrops()) {
511         if(gumdrop && gumdrop.style.display != 'none') {
512             gumdrop.style.setProperty('display', 'none', 'important');
513         }
514         return;
515     }
516
517     var style    = gumdrop.style;
518     if (!gumdrop || !el) { return; }
519     var display  = '';
520     var cstyle = doc.defaultView.getComputedStyle(el, '');
521     if (cstyle.display == 'none' ||
522         cstyle.visibility == 'hidden' ||
523         el.getAttribute('readonly') ||
524         el.getAttribute('disabled')
525         ) {
526         display = 'none';
527     }
528     if (style.display != display) {
529         style.setProperty('display', display, 'important');
530     }
531
532     /* Reposition the gumdrops incase the dom changed. */
533     var left = Math.max(1, el.offsetWidth-this.gumdrop_width);
534     var top  = el.offsetHeight;
535     var coord;
536     if (el.offsetParent === gumdrop.offsetParent) {
537         left += el.offsetLeft;
538         top  += el.offsetTop;
539     } else {
540         coord = ItsAllText.getContainingBlockOffset(el, gumdrop.offsetParent);
541         left += coord[0];
542         top  += coord[1];
543     }
544     if(left && top) {
545         left = [left,'px'].join('');
546         top  = [top,'px'].join('');
547         if(style.left != left) { style.setProperty('left', left, 'important');}
548         if(style.top != top)   { style.setProperty('top',  top, 'important');}
549     }
550 };
551
552 /**
553  * Creates a mostly unique hash of a string
554  * Most of this code is from:
555  *    http://developer.mozilla.org/en/docs/nsICryptoHash
556  * @param {String} some_string The string to hash.
557  * @returns {String} a hashed string.
558  */
559 CacheObj.prototype.hashString = function(some_string) {
560     var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
561     converter.charset = "UTF-8";
562     
563     /* result is the result of the hashing.  It's not yet a string,
564      * that'll be in retval.
565      * result.value will contain the array length
566      */
567     var result = {};
568     
569     /* data is an array of bytes */
570     var data = converter.convertToByteArray(some_string, result);
571     var ch   = Components.classes["@mozilla.org/security/hash;1"].createInstance(Components.interfaces.nsICryptoHash);
572     
573     ch.init(ch.MD5);
574     ch.update(data, data.length);
575     var hash = ch.finish(true);
576     
577     // return the two-digit hexadecimal code for a byte
578     var toHexString = function(charCode) {
579         return ("0" + charCode.toString(36)).slice(-2);
580     };
581     
582     // convert the binary hash data to a hex string.
583     var retval = [];
584     for(i in hash) {
585         retval[i] = toHexString(hash.charCodeAt(i));
586     }
587     
588     return(retval.join(""));
589 };