really make mandelbrot ready for add-on release
[mandelbrot.git] / xulapp / chrome / mandelbrot / content / mandelbrot.js
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
4  * The contents of this file are subject to the Mozilla Public License Version
5  * 1.1 (the "License"); you may not use this file except in compliance with
6  * the License. You may obtain a copy of the License at
7  * http://www.mozilla.org/MPL/
8  *
9  * Software distributed under the License is distributed on an "AS IS" basis,
10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11  * for the specific language governing rights and limitations under the
12  * License.
13  *
14  * The Original Code is KaiRo.at Mandelbrot, XULRunner version.
15  *
16  * The Initial Developer of the Original Code is
17  * Robert Kaiser <kairo@kairo.at>.
18  * Portions created by the Initial Developer are Copyright (C) 2008
19  * the Initial Developer. All Rights Reserved.
20  *
21  * Contributor(s):
22  *   Robert Kaiser <kairo@kairo.at>
23  *
24  * Alternatively, the contents of this file may be used under the terms of
25  * either the GNU General Public License Version 2 or later (the "GPL"), or
26  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27  * in which case the provisions of the GPL or the LGPL are applicable instead
28  * of those above. If you wish to allow use of your version of this file only
29  * under the terms of either the GPL or the LGPL, and not to allow others to
30  * use your version of this file under the terms of the MPL, indicate your
31  * decision by deleting the provisions above and replace them with the notice
32  * and other provisions required by the GPL or the LGPL. If you do not delete
33  * the provisions above, a recipient may use your version of this file under
34  * the terms of any one of the MPL, the GPL or the LGPL.
35  *
36  * ***** END LICENSE BLOCK ***** */
37
38 var gColorPalette = [];
39 var gPref = Components.classes["@mozilla.org/preferences-service;1"]
40                       .getService(Components.interfaces.nsIPrefService)
41                       .getBranch(null);
42 var gStartTime = 0;
43 var gMbrotBundle;
44 var gCurrentImageData;
45
46 function Startup() {
47   updateIterMenu();
48   updateAlgoMenu();
49   updatePaletteMenu();
50   gMbrotBundle = document.getElementById("mbrotBundle");
51   document.getElementById("statusLabel").value = gMbrotBundle.getString("statusEmpty");
52 }
53
54 function adjustCoordsAndDraw(aC_min, aC_max) {
55   let iWidth = 0;
56   try {
57     iWidth = gPref.getIntPref("mandelbrot.image.width");
58   }
59   catch (e) { }
60   if ((iWidth < 10) || (iWidth > 5000)) {
61     iWidth = 300;
62     gPref.setIntPref("mandelbrot.image.width", iWidth);
63   }
64   let iHeight = 0;
65   try {
66     iHeight = gPref.getIntPref("mandelbrot.image.height");
67   }
68   catch (e) { }
69   if ((iHeight < 10) || (iHeight > 5000)) {
70     iHeight = 300;
71     gPref.setIntPref("mandelbrot.image.height", iHeight);
72   }
73
74   // correct coordinates
75   if (aC_min.r < -2)
76     aC_min.r = -2;
77   if (aC_max.r > 2)
78     aC_max.r = 2;
79   if ((aC_min.r > 2) || (aC_max.r < -2) || (aC_min.r >= aC_max.r)) {
80     aC_min.r = -2.0; aC_max.r = 1.0;
81   }
82   if (aC_min.i < -2)
83     aC_min.i = -2;
84   if (aC_max.i > 2)
85     aC_max.i = 2;
86   if ((aC_min.i > 2) || (aC_max.i < -2) || (aC_min.i >= aC_max.i)) {
87     aC_min.i = -1.3; aC_max.i = 1.3;
88   }
89
90   let CWidth = aC_max.r - aC_min.r;
91   let CHeight = aC_max.i - aC_min.i;
92   let C_mid = new complex(aC_min.r + CWidth / 2, aC_min.i + CHeight / 2);
93
94   let CRatio = Math.max(CWidth / iWidth, CHeight / iHeight);
95
96   gPref.setCharPref("mandelbrot.last_image.Cr_min", C_mid.r - iWidth * CRatio / 2);
97   gPref.setCharPref("mandelbrot.last_image.Cr_max", C_mid.r + iWidth * CRatio / 2);
98   gPref.setCharPref("mandelbrot.last_image.Ci_min", C_mid.i - iHeight * CRatio / 2);
99   gPref.setCharPref("mandelbrot.last_image.Ci_max", C_mid.i + iHeight * CRatio / 2);
100
101   drawImage();
102 }
103
104 function drawImage() {
105   let canvas = document.getElementById("mbrotImage");
106   let context = canvas.getContext("2d");
107
108   document.getElementById("drawButton").hidden = true;
109
110   document.getElementById("statusLabel").value = gMbrotBundle.getString("statusDrawing");
111
112   let Cr_min = -2.0;
113   let Cr_max = 1.0;
114   try {
115     Cr_min = parseFloat(gPref.getCharPref("mandelbrot.last_image.Cr_min"));
116     Cr_max = parseFloat(gPref.getCharPref("mandelbrot.last_image.Cr_max"));
117   }
118   catch (e) { }
119   if ((Cr_min < -3) || (Cr_min > 2) ||
120       (Cr_max < -3) || (Cr_max > 2) || (Cr_min >= Cr_max)) {
121     Cr_min = -2.0; Cr_max = 1.0;
122   }
123   gPref.setCharPref("mandelbrot.last_image.Cr_min", Cr_min);
124   gPref.setCharPref("mandelbrot.last_image.Cr_max", Cr_max);
125
126   let Ci_min = -1.5;
127   let Ci_max = 1.5;
128   try {
129     Ci_min = parseFloat(gPref.getCharPref("mandelbrot.last_image.Ci_min"));
130     Ci_max = parseFloat(gPref.getCharPref("mandelbrot.last_image.Ci_max"));
131   }
132   catch (e) { }
133   if ((Ci_min < -2.5) || (Ci_min > 2.5) ||
134       (Ci_max < -2.5) || (Ci_max > 2.5) || (Ci_min >= Ci_max)) {
135     Ci_min = -1.5; Ci_max = 1.5;
136   }
137   gPref.setCharPref("mandelbrot.last_image.Ci_min", Ci_min);
138   gPref.setCharPref("mandelbrot.last_image.Ci_max", Ci_max);
139
140   let iterMax = gPref.getIntPref("mandelbrot.iteration_max");
141   let algorithm = gPref.getCharPref("mandelbrot.use_algorithm");
142
143   let iWidth = 0;
144   try {
145     iWidth = gPref.getIntPref("mandelbrot.image.width");
146   }
147   catch (e) { }
148   if ((iWidth < 10) || (iWidth > 5000)) {
149     iWidth = 300;
150     gPref.setIntPref("mandelbrot.image.width", iWidth);
151   }
152   let iHeight = 0;
153   try {
154     iHeight = gPref.getIntPref("mandelbrot.image.height");
155   }
156   catch (e) { }
157   if ((iHeight < 10) || (iHeight > 5000)) {
158     iHeight = 300;
159     gPref.setIntPref("mandelbrot.image.height", iHeight);
160   }
161
162   gCurrentImageData = {
163     C_min: new complex(Cr_min, Ci_min),
164     C_max: new complex(Cr_max, Ci_max),
165     iWidth: iWidth,
166     iHeight: iHeight,
167     iterMax: iterMax
168   };
169
170   canvas.width = iWidth;
171   canvas.height = iHeight;
172
173   context.fillStyle = "rgba(255, 255, 255, 127)";
174   context.fillRect(0, 0, canvas.width, canvas.height);
175
176   gStartTime = new Date();
177
178   drawLine(0, [Cr_min, Cr_max, Ci_min, Ci_max],
179               canvas, context, iterMax, algorithm);
180 }
181
182 function drawLine(line, dimensions, canvas, context, iterMax, algorithm) {
183   let Cr_min = dimensions[0];
184   let Cr_max = dimensions[1];
185   let Cr_scale = Cr_max - Cr_min;
186
187   let Ci_min = dimensions[2];
188   let Ci_max = dimensions[3];
189   let Ci_scale = Ci_max - Ci_min;
190
191   let pixels = [];
192   for (var img_y = line; img_y < canvas.height && img_y < line+8; img_y++)
193     for (let img_x = 0; img_x < canvas.width; img_x++) {
194       let C = new complex(Cr_min + (img_x / canvas.width) * Cr_scale,
195                           Ci_min + (img_y / canvas.height) * Ci_scale);
196       pixels.push.apply(pixels, drawPoint(context, img_x, img_y, C, iterMax, algorithm));
197     }
198   context.putImageData({width: canvas.width, height: pixels.length/4/canvas.width, data: pixels}, 0, line);
199
200   if (img_y < canvas.height)
201     setTimeout(drawLine, 0, img_y, dimensions, canvas, context, iterMax, algorithm);
202   else if (gStartTime)
203     EndCalc();
204 }
205
206 function EndCalc() {
207   let endTime = new Date();
208   let timeUsed = (endTime.getTime() - gStartTime.getTime()) / 1000;
209   document.getElementById("statusLabel").value =
210       gMbrotBundle.getFormattedString("statusTime", [timeUsed.toFixed(3)]);
211   gStartTime = 0;
212 }
213
214 function complex(aReal, aImag) {
215   this.r = aReal;
216   this.i = aImag;
217 }
218 complex.prototype = {
219   square: function() {
220     return new complex(this.r * this.r - this.i * this.i,
221                        2 * this.r * this.i);
222   },
223   dist: function() {
224     return Math.sqrt(this.r * this.r + this.i * this.i);
225   },
226   add: function(aComplex) {
227     return new complex(this.r + aComplex.r, this.i + aComplex.i);
228   }
229 }
230
231 function mandelbrotValueOO (aC, aIterMax) {
232   // this would be nice code in general but it looks like JS objects are too heavy for normal use.
233   let Z = new complex(0.0, 0.0);
234   for (var iter = 0; iter < aIterMax; iter++) {
235     Z = Z.square().add(aC);
236     if (Z.r * Z.r + Z.i * Z.i > 256) { break; }
237   }
238   return iter;
239 }
240
241 function mandelbrotValueNumeric (aC, aIterMax) {
242   // optimized numeric code for fast calculation
243   let Cr = aC.r, Ci = aC.i;
244   let Zr = 0.0, Zi = 0.0;
245   let Zr2 = Zr * Zr, Zi2 = Zi * Zi;
246   for (var iter = 0; iter < aIterMax; iter++) {
247     Zi = 2 * Zr * Zi + Ci;
248     Zr = Zr2 - Zi2 + Cr;
249
250     Zr2 = Zr * Zr; Zi2 = Zi * Zi;
251     if (Zr2 + Zi2 > 256) { break; }
252   }
253   return iter;
254 }
255
256 function getColor(aIterValue, aIterMax) {
257   let standardizedValue = Math.round(aIterValue * 1024 / aIterMax);
258   if (gColorPalette && gColorPalette.length)
259     return gColorPalette[standardizedValue];
260
261   // fallback to simple b/w if for some reason we don't have a palette
262   if (aIterValue == aIterMax)
263     return [0, 0, 0, 255];
264   else
265     return [255, 255, 255, 255];
266 }
267
268 function getColorPalette(palName) {
269   var palette = [];
270   switch (palName) {
271     case 'bw':
272       for (let i = 0; i < 1024; i++) {
273         palette[i] = [255, 255, 255, 255];
274       }
275       palette[1024] = [0, 0, 0, 255];
276       break;
277     case 'kairo':
278       // outer areas
279       for (let i = 0; i < 32; i++) {
280         let cc1 = Math.floor(i * 127 / 31);
281         let cc2 = 170 - Math.floor(i * 43 / 31);
282         palette[i] = [cc1, cc2, cc1, 255];
283       }
284       // inner areas
285       for (let i = 0; i < 51; i++) {
286         let cc = Math.floor(i * 170 / 50);
287         palette[32 + i] = [cc, 0, (170-cc), 255];
288       }
289       // corona
290       for (let i = 0; i < 101; i++) {
291         let cc = Math.floor(i * 200 / 100);
292         palette[83 + i] = [255, cc, 0, 255];
293       }
294       // inner corona
295       for (let i = 0; i < 201; i++) {
296         let cc1 = 255 - Math.floor(i * 85 / 200);
297         let cc2 = 200 - Math.floor(i * 30 / 200);
298         let cc3 = Math.floor(i * 170 / 200);
299         palette[184 + i] = [cc1, cc2, cc3, 255];
300       }
301       for (let i = 0; i < 301; i++) {
302         let cc1 = 170 - Math.floor(i * 43 / 300);
303         let cc2 = 170 + Math.floor(i * 85 / 300);
304         palette[385 + i] = [cc1, cc1, cc2, 255];
305       }
306       for (let i = 0; i < 338; i++) {
307         let cc = 127 + Math.floor(i * 128 / 337);
308         palette[686 + i] = [cc, cc, 255, 255];
309       }
310       palette[1024] = [0, 0, 0, 255];
311       break;
312     case 'rainbow-linear1':
313       for (let i = 0; i < 256; i++) {
314         palette[i] = [i, 0, 0, 255];
315         palette[256 + i] = [255, i, 0, 255];
316         palette[512 + i] = [255 - i, 255, i, 255];
317         palette[768 + i] = [i, 255-i, 255, 255];
318       }
319       palette[1024] = [0, 0, 0, 255];
320       break;
321     case 'rainbow-squared1':
322       for (let i = 0; i < 34; i++) {
323         let cc = Math.floor(i * 255 / 33);
324         palette[i] = [cc, 0, 0, 255];
325       }
326       for (let i = 0; i < 137; i++) {
327         let cc = Math.floor(i * 255 / 136);
328         palette[34 + i] = [255, cc, 0, 255];
329       }
330       for (let i = 0; i < 307; i++) {
331         let cc = Math.floor(i * 255 / 306);
332         palette[171 + i] = [255 - cc, 255, cc, 255];
333       }
334       for (let i = 0; i < 546; i++) {
335         let cc = Math.floor(i * 255 / 545);
336         palette[478 + i] = [cc, 255 - cc, 255, 255];
337       }
338       palette[1024] = [0, 0, 0, 255];
339       break;
340     case 'rainbow-linear2':
341       for (let i = 0; i < 205; i++) {
342         let cc = Math.floor(i * 255 / 204);
343         palette[i] = [255, cc, 0, 255];
344         palette[204 + i] = [255 - cc, 255, 0, 255];
345         palette[409 + i] = [0, 255, cc, 255];
346         palette[614 + i] = [0, 255 - cc, 255, 255];
347         palette[819 + i] = [cc, 0, 255, 255];
348       }
349       palette[1024] = [0, 0, 0, 255];
350       break;
351     case 'rainbow-squared2':
352       for (let i = 0; i < 19; i++) {
353         let cc = Math.floor(i * 255 / 18);
354         palette[i] = [255, cc, 0, 255];
355       }
356       for (let i = 0; i < 74; i++) {
357         let cc = Math.floor(i * 255 / 73);
358         palette[19 + i] = [255 - cc, 255, 0, 255];
359       }
360       for (let i = 0; i < 168; i++) {
361         let cc = Math.floor(i * 255 / 167);
362         palette[93 + i] = [0, 255, cc, 255];
363       }
364       for (let i = 0; i < 298; i++) {
365         let cc = Math.floor(i * 255 / 297);
366         palette[261 + i] = [0, 255 - cc, 255, 255];
367       }
368       for (let i = 0; i < 465; i++) {
369         let cc = Math.floor(i * 255 / 464);
370         palette[559 + i] = [cc, 0, 255, 255];
371       }
372       palette[1024] = [0, 0, 0, 255];
373       break;
374   }
375   /*
376      'Standard-Palette (QB-Colors)
377      For i = 0 To 1024
378          xx = CInt(i * 500 / 1024 + 2)
379          If xx <= 15 Then clr = xx
380          If xx > 15 Then clr = CInt(Sqr((xx - 15 + 1) * 15 ^ 2 / 485))
381          If xx >= 500 Then clr = 0
382          palette(i) = QBColor(clr)
383      Next
384   */
385   return palette;
386 }
387
388 function drawPoint(context, img_x, img_y, C, iterMax, algorithm) {
389   var itVal;
390   switch (algorithm) {
391     case 'oo':
392       itVal = mandelbrotValueOO(C, iterMax);
393       break;
394     case 'numeric':
395     default:
396       itVal = mandelbrotValueNumeric(C, iterMax);
397       break;
398   }
399   return getColor(itVal, iterMax);
400 }
401
402 /***** pure UI functions *****/
403
404 var zoomstart;
405 var imgBackup;
406
407 function mouseevent(etype, event) {
408   let canvas = document.getElementById("mbrotImage");
409   let context = canvas.getContext("2d");
410   switch (etype) {
411     case 'down':
412       if (event.button == 0) {
413         // left button - start dragzoom
414         zoomstart = {x: event.clientX - canvas.offsetLeft,
415                      y: event.clientY - canvas.offsetTop};
416         imgBackup = context.getImageData(0, 0, canvas.width, canvas.height);
417       }
418       break;
419     case 'up':
420       if (event.button == 0 && zoomstart) {
421         context.putImageData(imgBackup, 0, 0);
422         let zoomend = {x: event.clientX - canvas.offsetLeft,
423                        y: event.clientY - canvas.offsetTop};
424
425         // make sure zoomend is bigger than zoomstart
426         if ((zoomend.x == zoomstart.x) || (zoomend.y == zoomstart.y)) {
427           // cannot zoom what has no area, discard it
428           zoomstart = undefined;
429           return;
430         }
431         if (zoomend.x < zoomstart.x)
432           [zoomend.x, zoomstart.x] = [zoomstart.x, zoomend.x];
433         if (zoomend.y < zoomstart.y)
434           [zoomend.y, zoomstart.y] = [zoomstart.y, zoomend.y];
435
436         let prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
437                                 .getService(Components.interfaces.nsIPromptService);
438         let ok = prompts.confirm(null, gMbrotBundle.getString("zoomConfirmTitle"),
439                                  gMbrotBundle.getString("zoomConfirmLabel"));
440         // ok is now true if OK was clicked, and false if cancel was clicked
441         if (ok) {
442           // determine new "coordinates"
443           let CWidth = gCurrentImageData.C_max.r - gCurrentImageData.C_min.r;
444           let CHeight = gCurrentImageData.C_max.i - gCurrentImageData.C_min.i;
445           let newC_min = new complex(
446               gCurrentImageData.C_min.r + zoomstart.x / gCurrentImageData.iWidth * CWidth,
447               gCurrentImageData.C_min.i + zoomstart.y / gCurrentImageData.iHeight * CHeight);
448           let newC_max = new complex(
449               gCurrentImageData.C_min.r + zoomend.x / gCurrentImageData.iWidth * CWidth,
450               gCurrentImageData.C_min.i + zoomend.y / gCurrentImageData.iHeight * CHeight);
451
452           adjustCoordsAndDraw(newC_min, newC_max);
453         }
454       }
455       zoomstart = undefined;
456       break;
457     case 'move':
458       if (event.button == 0 && zoomstart) {
459         context.putImageData(imgBackup, 0, 0);
460         context.strokeStyle = "rgb(255,255,31)";
461         context.strokeRect(zoomstart.x, zoomstart.y,
462                            event.clientX - canvas.offsetLeft - zoomstart.x,
463                            event.clientY - canvas.offsetTop - zoomstart.y);
464       }
465     break;
466   }
467 }
468
469 function saveImage() {
470   const nsIFilePicker = Components.interfaces.nsIFilePicker;
471   let fp = null;
472   try {
473     fp = Components.classes["@mozilla.org/filepicker;1"]
474                    .createInstance(nsIFilePicker);
475   } catch (e) {}
476   if (!fp) return;
477   let promptString = gMbrotBundle.getString("savePrompt");
478   fp.init(window, promptString, nsIFilePicker.modeSave);
479   fp.appendFilter(gMbrotBundle.getString("pngFilterName"), "*.png");
480   fp.defaultString = "mandelbrot.png";
481
482   let fpResult = fp.show();
483   if (fpResult != nsIFilePicker.returnCancel) {
484     saveCanvas(document.getElementById("mbrotImage"), fp.file);
485   }
486 }
487
488 function exitMandelbrot() {
489   var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
490                           .getService(Components.interfaces.nsIXULAppInfo);
491   if (appInfo.ID == "mandelbrot@kairo.at")
492     quitApp(false);
493   else
494     window.close();
495 }
496
497 function updateBookmarkMenu(aParent) {
498   document.getElementById("bookmarkSave").disabled =
499     (!document.getElementById("drawButton").hidden || (gStartTime > 0));
500
501   while (aParent.hasChildNodes() &&
502          aParent.lastChild.id != "bookmarkSeparator")
503     aParent.removeChild(aParent.lastChild);
504
505   let file = Components.classes["@mozilla.org/file/directory_service;1"]
506                        .getService(Components.interfaces.nsIProperties)
507                        .get("ProfD", Components.interfaces.nsIFile);
508   file.append("mandelbookmarks.sqlite");
509   if (file.exists()) {
510     let connection = Components.classes["@mozilla.org/storage/service;1"]
511                                .getService(Components.interfaces.mozIStorageService)
512                                .openDatabase(file);
513     try {
514       if (connection.tableExists("bookmarks")) {
515         let statement = connection.createStatement(
516             "SELECT name,ROWID FROM bookmarks ORDER BY ROWID ASC");
517         while (statement.executeStep()) {
518           let newItem = aParent.appendChild(document.createElement("menuitem"));
519           newItem.setAttribute("label", statement.getString(0));
520           newItem.setAttribute("bmRowID", statement.getString(1));
521         }
522         statement.reset();
523         statement.finalize();
524         return;
525       }
526     } finally {
527       connection.close();
528     }
529   }
530   // Create the "Nothing Available" Menu item and disable it.
531   let na = aParent.appendChild(document.createElement("menuitem"));
532   na.setAttribute("label", gMbrotBundle.getString("noBookmarks"));
533   na.setAttribute("disabled", "true");
534 }
535
536 function callBookmark(evtarget) {
537   if (evtarget.id == "bookmarkSave" || evtarget.id == "bookmarkSeparator")
538     return;
539   if (evtarget.id == "bookmarkOverview") {
540     adjustCoordsAndDraw(new complex(0,0), new complex(0,0));
541     return;
542   }
543
544   if (evtarget.getAttribute('bmRowID')) {
545     let iterMax = 0;
546     let C_min = null;
547     let C_max = null;
548
549     let file = Components.classes["@mozilla.org/file/directory_service;1"]
550                          .getService(Components.interfaces.nsIProperties)
551                          .get("ProfD", Components.interfaces.nsIFile);
552     file.append("mandelbookmarks.sqlite");
553     let connection = Components.classes["@mozilla.org/storage/service;1"]
554                                .getService(Components.interfaces.mozIStorageService)
555                                .openDatabase(file);
556     let statement = connection.createStatement(
557         "SELECT iteration_max,Cr_min,Cr_max,Ci_min,Ci_max FROM bookmarks WHERE ROWID=?1");
558     statement.bindStringParameter(0, evtarget.getAttribute('bmRowID'));
559     while (statement.executeStep()) {
560       iterMax = statement.getInt32(0);
561       C_min = new complex(statement.getDouble(1), statement.getDouble(3));
562       C_max = new complex(statement.getDouble(2), statement.getDouble(4));
563     }
564     statement.finalize();
565     connection.close();
566
567     if (iterMax && C_min && C_max) {
568       gPref.setIntPref("mandelbrot.iteration_max", iterMax);
569       adjustCoordsAndDraw(C_min, C_max);
570     }
571   }
572 }
573
574 function saveBookmark() {
575   // retrieve wanted bookmark name with a prompt
576   let prompts = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
577                           .getService(Components.interfaces.nsIPromptService);
578   let input = {value: ""}; // empty default value
579   let ok = prompts.prompt(null, gMbrotBundle.getString("saveBookmarkTitle"), gMbrotBundle.getString("saveBookmarkLabel"), input, null, {});
580   // ok is true if OK is pressed, false if Cancel. input.value holds the value of the edit field if "OK" was pressed.
581   if (!ok || !input.value)
582     return
583
584   let bmName = input.value;
585
586   // Open or create the bookmarks database.
587   let file = Components.classes["@mozilla.org/file/directory_service;1"]
588                        .getService(Components.interfaces.nsIProperties)
589                        .get("ProfD", Components.interfaces.nsIFile);
590   file.append("mandelbookmarks.sqlite");
591   let connection = Components.classes["@mozilla.org/storage/service;1"]
592                              .getService(Components.interfaces.mozIStorageService)
593                              .openDatabase(file);
594   connection.beginTransaction();
595   if (!connection.tableExists("bookmarks"))
596     connection.createTable("bookmarks", "name TEXT, iteration_max INTEGER, Cr_min REAL, Cr_max REAL, Ci_min REAL, Ci_max REAL");
597   // NULL. The value is a NULL value.
598   // INTEGER. The value is a signed integer, stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude of the value.
599   // REAL. The value is a floating point value, stored as an 8-byte IEEE floating point number.
600   // TEXT. The value is a text string, stored using the database encoding (UTF-8, UTF-16BE or UTF-16-LE).
601
602   // Put value of the current image into the bookmarks table
603   let statement = connection.createStatement(
604       "INSERT INTO bookmarks (name,iteration_max,Cr_min,Cr_max,Ci_min,Ci_max) VALUES (?1,?2,?3,?4,?5,?6)");
605   statement.bindStringParameter(0, bmName);
606   statement.bindStringParameter(1, gCurrentImageData.iterMax);
607   statement.bindStringParameter(2, gCurrentImageData.C_min.r);
608   statement.bindStringParameter(3, gCurrentImageData.C_max.r);
609   statement.bindStringParameter(4, gCurrentImageData.C_min.i);
610   statement.bindStringParameter(5, gCurrentImageData.C_max.i);
611   statement.execute();
612   statement.finalize();
613   connection.commitTransaction();
614   connection.close();
615 }
616
617 function updateIterMenu() {
618   let currentIter = 0;
619   try {
620     currentIter = gPref.getIntPref("mandelbrot.iteration_max");
621   }
622   catch(e) { }
623   if (currentIter < 10) {
624     currentIter = 500;
625     setIter(currentIter);
626   }
627
628   let popup = document.getElementById("menu_iterPopup");
629   let item = popup.firstChild;
630   while (item) {
631     if (item.getAttribute("name") == "iter") {
632       if (item.getAttribute("value") == currentIter)
633         item.setAttribute("checked","true");
634       else
635         item.removeAttribute("checked");
636     }
637     item = item.nextSibling;
638   }
639 }
640
641 function setIter(aIter) {
642   gPref.setIntPref("mandelbrot.iteration_max", aIter);
643 }
644
645 function updatePaletteMenu() {
646   let currentPalette = '';
647   try {
648     currentPalette = gPref.getCharPref("mandelbrot.color_palette");
649   }
650   catch(e) { }
651   if (!currentPalette.length) {
652     currentPalette = 'kairo';
653     setPalette(currentPalette);
654   }
655   if (!gColorPalette || !gColorPalette.length)
656     gColorPalette = getColorPalette(currentPalette);
657
658   let popup = document.getElementById("menu_palettePopup");
659   let item = popup.firstChild;
660   while (item) {
661     if (item.getAttribute("name") == "palette") {
662       if (item.getAttribute("value") == currentPalette)
663         item.setAttribute("checked", "true");
664       else
665         item.removeAttribute("checked");
666     }
667     item = item.nextSibling;
668   }
669 }
670
671 function setPalette(aPaletteID) {
672   gPref.setCharPref("mandelbrot.color_palette", aPaletteID);
673   gColorPalette = getColorPalette(aPaletteID);
674 }
675
676 function imgSettings() {
677   window.openDialog("chrome://mandelbrot/content/image-settings.xul");
678 }
679
680 function updateDebugMenu() {
681   var jitMenuItem = document.getElementById("jitEnabled");
682   jitMenuItem.setAttribute("checked", gPref.getBoolPref("javascript.options.jit.chrome"));
683 }
684
685 function toggleJITState(jitMenuItem) {
686   var jitEnabled = !gPref.getBoolPref("javascript.options.jit.chrome");
687   gPref.setBoolPref("javascript.options.jit.chrome", jitEnabled)
688   jitMenuItem.setAttribute("checked", jitEnabled? "true" : "false");
689 }
690
691 function updateAlgoMenu() {
692   let currentAlgo = '';
693   try {
694     currentAlgo = gPref.getCharPref("mandelbrot.use_algorithm");
695   }
696   catch(e) { }
697   if (!currentAlgo.length) {
698     currentAlgo = 'numeric';
699     setAlgorithm(currentAlgo);
700   }
701
702   let popup = document.getElementById("menu_algoPopup");
703   let item = popup.firstChild;
704   while (item) {
705     if (item.getAttribute("name") == "algorithm") {
706       if (item.getAttribute("value") == currentAlgo)
707         item.setAttribute("checked", "true");
708       else
709         item.removeAttribute("checked");
710     }
711     item = item.nextSibling;
712   }
713 }
714
715 function setAlgorithm(algoID) {
716   gPref.setCharPref("mandelbrot.use_algorithm", algoID);
717 }
718
719 function addonsManager(aPane) {
720   let theEM = Components.classes["@mozilla.org/appshell/window-mediator;1"]
721                         .getService(Components.interfaces.nsIWindowMediator)
722                         .getMostRecentWindow("Extension:Manager");
723   if (theEM) {
724     theEM.focus();
725     if (aPane)
726       theEM.showView(aPane);
727     return;
728   }
729
730   const EMURL = "chrome://mozapps/content/extensions/extensions.xul";
731   const EMFEATURES = "all,dialog=no";
732   if (aPane)
733     window.openDialog(EMURL, "", EMFEATURES, aPane);
734   else
735     window.openDialog(EMURL, "", EMFEATURES);
736 }
737
738 function errorConsole() {
739   toOpenWindowByType("global:console", "chrome://global/content/console.xul");
740 }
741
742 /***** helper functions from external sources *****/
743
744 // function below is based on http://developer.mozilla.org/en/docs/Code_snippets:Canvas
745 // custom modifications:
746 //   - use "a"-prefix on function arguments
747 //   - take an nsILocalFile as aDestFile argument
748 //   - always do silent download
749 function saveCanvas(aCanvas, aDestFile) {
750   // create a data url from the canvas and then create URIs of the source and targets  
751   var io = Components.classes["@mozilla.org/network/io-service;1"]
752                      .getService(Components.interfaces.nsIIOService);
753   var source = io.newURI(aCanvas.toDataURL("image/png", ""), "UTF8", null);
754
755   // prepare to save the canvas data
756   var persist = Components.classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
757                           .createInstance(Components.interfaces.nsIWebBrowserPersist);
758
759   persist.persistFlags = Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
760   persist.persistFlags |= Components.interfaces.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
761
762   // save the canvas data to the file
763   persist.saveURI(source, null, null, null, null, aDestFile);
764 }
765
766 // function below is from http://developer.mozilla.org/en/docs/How_to_Quit_a_XUL_Application
767 function quitApp(aForceQuit) {
768   var appStartup = Components.classes['@mozilla.org/toolkit/app-startup;1']
769                              .getService(Components.interfaces.nsIAppStartup);
770
771   // eAttemptQuit will try to close each XUL window, but the XUL window can cancel the quit
772   // process if there is unsaved data. eForceQuit will quit no matter what.
773   var quitSeverity = aForceQuit ? Components.interfaces.nsIAppStartup.eForceQuit :
774                                   Components.interfaces.nsIAppStartup.eAttemptQuit;
775   appStartup.quit(quitSeverity);
776 }
777
778 // functions below are from comm-central/suite/common/tasksOverlay.js
779 function toOpenWindow(aWindow) {
780   try {
781     // Try to focus the previously focused window e.g. message compose body
782     aWindow.document.commandDispatcher.focusedWindow.focus();
783   } catch (e) {
784     // e.g. full-page plugin or non-XUL document; just raise the top window
785     aWindow.focus();
786   }
787 }
788
789 function toOpenWindowByType(inType, uri, features) {
790   // don't do several loads in parallel
791   if (uri in window)
792     return;
793
794   var topWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"]
795                             .getService(Components.interfaces.nsIWindowMediator)
796                             .getMostRecentWindow(inType);
797   if ( topWindow )
798     toOpenWindow( topWindow );
799   else {
800     // open the requested window, but block it until it's fully loaded
801     function newWindowLoaded(event) {
802       // make sure that this handler is called only once
803       window.removeEventListener("unload", newWindowLoaded, false);
804       window[uri].removeEventListener("load", newWindowLoaded, false);
805       delete window[uri];
806     }
807     // remember the newly loading window until it's fully loaded
808     // or until the current window passes away
809     window[uri] = window.openDialog(uri, "", features || "all,dialog=no");
810     window[uri].addEventListener("load", newWindowLoaded, false);
811     window.addEventListener("unload", newWindowLoaded, false);
812   }
813 }