fixing some typos
[firefox-gnome-globalmenu:firefox-gnome-globalmenu.git] / chrome / content / overlay.js
1
2 var Globalmenubar = {
3         /* Configuration options */
4         /* Min time between menu updates */
5         batch_interval: 500,
6         /* Send all images (gtk stock icons are always send, as they're cheap) */
7         send_images: true,
8
9         /* Internal */
10         /* Instance of the native component */
11         serv: null,
12         /* Instance of preferences service main branch */
13         prefs: null,
14         /* Key names string bundle */
15         keys_sb: null,
16         /* Modifier names string bundle */
17         mods_sb: null,
18         /* Whether window associated to this instance is focused */
19         focused: false,
20         /* Update menu timeout handle */
21         update_timeout: 0,
22         /* Last "random" menu id used */
23         rand_id: 0,
24         /* Dictionary mapping all current known menu ids to their actions */
25         id_list: [],
26         /* Dictionary mapping all current known menu ids with URI to their URIs */
27         id_uris: [],
28         /* Cache of base64'd images */
29         img_cache: [],
30
31         /* Some stored preferences */
32         _accelKey: false,
33         _alwaysAppendAccessKeys: false,
34         _insertSeparatorBeforeAccessKeys: false,
35         _ellipsis: "...",
36
37         /* Get a nsIBaseWindow from the DOM window object */
38         getBaseWindow: function(w) {
39                 const Ci = Components.interfaces;
40                 var webNav = w.getInterface(Ci.nsIWebNavigation);
41                 var treeItem = webNav.QueryInterface(Ci.nsIDocShellTreeItem);
42                 var treeOwner = treeItem.treeOwner;
43                 var baseWindow = treeOwner.QueryInterface(Ci.nsIBaseWindow);
44                 return baseWindow;
45         },
46
47         /* Called when browser windows is loaded. Window is not realized yet. */
48         onLoad: function() {
49                 const Cc = Components.classes;
50                 const Ci = Components.interfaces;
51
52                 /* Instantiate native component first */
53                 this.serv = Cc["@javispedro.com/GlobalMenu;1"].createInstance(Ci.IGlobalMenu);
54
55                 this.serv.window = this.getBaseWindow(window);
56                 this.serv.listener = this;
57
58                 /* Global preferences service */
59                 this.prefs = Cc["@mozilla.org/preferences-service;1"].
60                                                 getService(Ci.nsIPrefService).getBranch("");
61
62                 /* String bundles from where to get translations for Alt, Control, .. */
63                 var sb_serv = Cc['@mozilla.org/intl/stringbundle;1'].
64                                                 getService(Ci.nsIStringBundleService);
65                 this.keys_sb = sb_serv.createBundle('chrome://global/locale/keys.properties');
66                 this.mods_sb = sb_serv.createBundle('chrome://global-platform/locale/platformKeys.properties');
67
68                 /* Subscribe to edit menu changes */
69                 document.getElementById("editMenuCommandSetAll").addEventListener("commandupdate",
70                         function(event) { Globalmenubar.onEditStateChange(event); }, false);
71
72                 /* Subscribe to Tab close event */
73                 gBrowser.tabContainer.addEventListener("TabClose",
74                         function(event) { Globalmenubar.onTabChange(event); }, false);
75
76                 /* Subscribe to history event notifications */
77                 var history = Cc["@mozilla.org/browser/nav-history-service;1"].
78                                                 getService(Ci.nsINavHistoryService);
79                 this.history_listener.menu = this;
80                 history.addObserver(this.history_listener, false);
81         },
82
83         /* Called when browser windows get focus. */
84         onFocus: function(e) {
85                 this.focused = true;
86                 this.queueUpdateWindowMenu();
87         },
88
89         onBlur: function(e) {
90                 this.focused = false;
91                 if (this.update_timeout) {
92                         clearTimeout(this.update_timeout);
93                         this.update_timeout = 0;
94                 }
95         },
96
97         onEditStateChange: function(e) {
98                 this.queueUpdateWindowMenu();
99         },
100
101         onTabChange: function(e) {
102                 this.queueUpdateWindowMenu();
103         },
104
105         history_listener: {
106                 menu: null,
107                 onBeginUpdateBatch: function() {
108                 },
109                 onEndUpdateBatch: function() {
110                 },
111                 onVisit: function(aURI, aVisitID, aTime, aSessionID, aReferringID, aTransitionType) {
112                         this.menu.queueUpdateWindowMenu();
113                 },
114                 onTitleChanged: function(aURI, aPageTitle) {
115                 },
116                 onDeleteURI: function(aURI) {
117                 },
118                 onClearHistory: function() {
119                         this.menu.queueUpdateWindowMenu();
120                 },
121                 onPageChanged: function(aURI, aWhat, aValue) {
122                 },
123                 onPageExpired: function(aURI, aVisitTime, aWholeEntry) {
124                 },
125                 QueryInterface: function(iid) {
126                         const Ci = Components.interfaces;
127                         if (iid.equals(Ci.nsINavHistoryObserver) ||
128                                 iid.equals(Ci.nsISupports)) {
129                                 return this;
130                         }
131                         throw Cr.NS_ERROR_NO_INTERFACE;
132                 }
133         },
134
135         /* Cache some preferences every time the menu is update */
136         updatePrefs: function() {
137                 const Ci = Components.interfaces;
138
139                 this._accelKey = this.prefs.getIntPref("ui.key.accelKey");
140                 this._alwaysAppendAccessKeys =
141                         this.prefs.getComplexValue("intl.menuitems.alwaysappendaccesskeys",
142                         Ci.nsIPrefLocalizedString).data == "true";
143                 this._insertSeparatorBeforeAccessKeys =
144                         this.prefs.getComplexValue(
145                                 "intl.menuitems.insertseparatorbeforeaccesskeys",
146                                 Ci.nsIPrefLocalizedString).data == "true";
147                 this._ellipsis = this.prefs.getComplexValue("intl.ellipsis",
148                         Ci.nsIPrefLocalizedString).data;
149         },
150
151         /* Escape a string so that it is a valid XML attribute value. */
152         escapeXml: function(s) {
153                 if (!s) return "";
154                 return s.replace(/&/g, '&').replace(/"/g, '"').
155                         replace(/</g, '&lt;').replace(/>/g, '&gt;');
156         },
157
158         /* Escape a string so that it is a valid regular expression literal. */
159         escapePreg: function(s) {
160                 if (!s) return "";
161                 var regex = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g');
162                 return s.replace(regex, "\\$&");
163         },
164
165         /* Convert from XUL mnemonic format to Gtk+ */
166         /* https://developer.mozilla.org/En/XUL_Tutorial/Accesskey_display_rules */
167         createLabelWithAccessKey: function(label, accesskey) {
168                 label = label.replace(/_/g, "__");
169                 if (accesskey) {
170                         var regex = new RegExp(this.escapePreg(accesskey), "i");
171                         if (!this._alwaysAppendAccessKeys && label.search(regex) >= 0) {
172                                 /* "Inline" access key (ex. "_File") */
173                                 return label.replace(regex, "_$&");
174                         } else {
175                                 /* Appended access key (ex. "File (_A)") */
176                                 if (label.slice(-this._ellipsis.length) == this._ellipsis) {
177                                         /* Label ends with ellipsis */
178                                         return label.substring(0, label.length-this._ellipsis.length) +
179                                                 (this._insertSeparatorBeforeAccessKeys ? " " : "") +
180                                                 "(_" + accesskey.toUpperCase() + ")" +
181                                                 this._ellipsis;
182                                 } else {
183                                         return label +
184                                                 (this._insertSeparatorBeforeAccessKeys ? " " : "") +
185                                                 "(_" + accesskey.toUpperCase() + ")";
186                                 }
187                         }
188                 } else {
189                         /* No access key */
190                         return label;
191                 }
192         },
193
194         /* Convert from XUL image format to Globalmenu */
195         getImage: function(item) {
196                 const Ci = Components.interfaces;
197
198                 var image_uri = item.getAttribute("image");
199                 if (!image_uri) {
200                         var style_regex = /url\("(.*?)"\)/;
201                         var image_style = window.getComputedStyle(item, null).getPropertyValue("list-style-image");
202                         var res = style_regex.exec(image_style);
203                         if (res) {
204                                 image_uri = res[1];
205                         }
206                 }
207                 if (image_uri) {
208                         /* Test if it is a Gtk stock image first */
209                         var regex = /moz-icon:\/\/stock\/(.*?)\?/;
210                         var res = regex.exec(image_uri);
211                         if (res) {
212                                 return res[1];
213                         }
214                         /* Only convert the rest if configured to */
215                         if (this.send_images) {
216                                 /* Handle image clipping */
217                                 var rect = [0, 0, 0, 0];
218                                 var region = window.getComputedStyle(item, null).getPropertyValue("-moz-image-region");
219                                 var cache_id = image_uri;
220                                 if (region && region != "auto") {
221                                         var region_regex = /rect\((\d+)px, (\d+)px, (\d+)px, (\d+)px\)/;
222                                         var values = region_regex.exec(region);
223                                         if (values) {
224                                                 rect[0] = parseInt(values[4]); /* Left */
225                                                 rect[1] = parseInt(values[1]); /* Top */
226                                                 rect[2] = parseInt(values[2]) - rect[0]; /* Right - Left*/
227                                                 rect[3] = parseInt(values[3]) - rect[1]; /* Bottom - Top */
228                                         }
229                                         cache_id += "#" + region;
230                                 }
231
232                                 /* Check image cache first */
233                                 if (this.img_cache[cache_id]) {
234                                         return "pixbuf:" + this.img_cache[cache_id];
235                                 }
236
237                                 /* Use an HTML image element to load the image */
238                                 var img_node = new Image();
239                                 img_node.src = image_uri;
240                                 var request = img_node
241                                         .QueryInterface(Ci.nsIImageLoadingContent)
242                                         .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
243                                 var container = request.image;
244                                 if (container) {
245                                         /* Call into native code to encode it as GdkPixbuf */
246                                         var image_data = this.serv.serializeImage(container,
247                                                 rect[0], rect[1], rect[2], rect[3]);
248                                         this.img_cache[cache_id] = image_data;
249                                         return "pixbuf:" + image_data;
250                                 } else {
251                                         /* Image is not loaded yet. Let's not worry about it. */
252                                         return false;
253                                 }
254                         }
255                 }
256                 return false;
257         },
258
259         isCmdDisabled: function(cmd) {
260                 var node = document.getElementById(cmd);
261                 if (node.getAttribute("disabled")) {
262                         return true;
263                 } else {
264                         return false;
265                 }
266         },
267
268         /* Create human-readable name for a menuitem accelerator */
269         getAccel: function(item) {
270                 if (item.getAttribute("acceltext")) {
271                         return item.getAttribute("acceltext");
272                 }
273
274                 var keyId = item.getAttribute("key");
275                 if (!keyId) return false;
276                 var key = document.getElementById(keyId);
277                 if (!key) return false;
278
279                 if (key.getAttribute("keytext")) {
280                         return key.getAttribute("keytext");
281                 }
282
283                 var accel="";
284                 var mods = key.getAttribute("modifiers");
285
286                 if (mods) {
287                         mods = mods.split(/[\s,]+/);
288                         for (i in mods) {
289                                 var mod = mods[i];
290                                 if (mod == "accel") {
291                                         /* Read platform default accelerator key from prefs */
292                                         switch (this._accelKey) {
293                                                 case KeyEvent.DOM_VK_SHIFT:
294                                                         mod = "shift";
295                                                         break;
296                                                 case KeyEvent.DOM_VK_CONTROL:
297                                                         mod = "control";
298                                                         break;
299                                                 case KeyEvent.DOM_VK_ALT:
300                                                         mod = "alt";
301                                                         break;
302                                                 case KeyEvent.DOM_VK_META:
303                                                         mod = "meta";
304                                                         break;
305                                         }
306                                 }
307                                 switch (mod) {
308                                         case "shift":
309                                                 accel += this.mods_sb.GetStringFromName("VK_SHIFT");
310                                                 break;
311                                         case "alt":
312                                                 accel += this.mods_sb.GetStringFromName("VK_ALT");
313                                                 break;
314                                         case "meta":
315                                                 accel += this.mods_sb.GetStringFromName("VK_META");
316                                                 break;
317                                         case "control":
318                                                 accel += this.mods_sb.GetStringFromName("VK_CONTROL");
319                                                 break;
320                                         default:
321                                                 continue;
322                                 }
323                                 accel += this.mods_sb.GetStringFromName("MODIFIER_SEPARATOR");
324                         }
325                 }
326
327                 if (key.getAttribute("key")) {
328                         return accel + key.getAttribute("key").toUpperCase();
329                 } else if (key.getAttribute("keycode")) {
330                         var keycode = key.getAttribute("keycode");
331                         return accel + this.keys_sb.GetStringFromName(keycode);
332                 }
333         },
334
335         /* Recursively descend into a XUL menu, return globalmenu xml string. */
336         genMenu: function(menu_id, menu, placesParent) {
337                 var popup = menu.firstChild;
338                 var xml = "";
339
340                 var rNode = null;
341                 if (popup.getAttribute("type") == "places") {
342                         /* This menu is managed by places; refresh it now. */
343                         rNode = popup.getResultNode();
344                         rNode.containerOpen = true;
345                         popup._rebuild(popup);
346                         placesParent = popup;
347                 } else if (placesParent && popup.getAttribute("placespopup")) {
348                         /* This menu is a child of a menu managed by places; refresh it now. */
349                         rNode = popup._resultNode;
350                         rNode.containerOpen = true;
351                         placesParent._rebuild(popup);
352                 }
353
354                 xml += "<menu>";
355                 for (var item=popup.firstChild; item; item=item.nextSibling) {
356                         var iid = item.id;
357                         var type = "d";
358                         var state = false;
359                         var disabled = item.disabled;
360                         var label = this.createLabelWithAccessKey(item.getAttribute("label"), item.accessKey);
361                         var image = this.getImage(item);
362                         var accel = this.getAccel(item);
363
364                         if (!iid) {
365                                 /* Everyone needs an id. */
366                                 iid = "r" + this.rand_id++;
367                         }
368
369                         if (item.tagName == "menuseparator") {
370                                 type="s";
371                         } else if (item.tagName == "menu") {
372                                 if (image) {
373                                         type="i";
374                                 }
375                         } else if (item.tagName == "menuitem") {
376                                 if (image) {
377                                         type="i";
378                                 }
379                                 if (item.getAttribute("type") == "checkbox") {
380                                         type="c";
381                                         state = item.getAttribute("checked") ? "t" : "f";
382                                 }
383                                 if (item.getAttribute("command")) {
384                                         disabled = this.isCmdDisabled(item.getAttribute("command"));
385                                 }
386                                 this.id_list[menu_id + "/" + iid] = item;
387                                 if (rNode && item.node && PlacesUtils.nodeIsURI(item.node)) {
388                                         /* This comes from a Places node (aka is a bookmark) */
389                                         this.id_uris[menu_id + "/" + iid] = item.node.uri;
390                                 }
391                         }
392                         xml += "<item id=\"" + iid + "\" " +
393                                         "label=\"" + this.escapeXml(label) + "\" " +
394                                         "type=\"" + type + "\" " +
395                                         (disabled ? "sensitive=\"false\" " : "") +
396                                         (state ? "state=\"" + state + "\" " : "") +
397                                         (image ? "icon=\"" + image + "\" " : "") +
398                                         (accel ? "accel=\"" + accel + "\" " : "") +
399                                 ">";
400                         if (item.tagName == "menu" &&
401                                 item.firstChild && item.firstChild.tagName == "menupopup") {
402                                 xml += this.genMenu(menu_id + "/" + iid, item, placesParent);
403                         }
404                         xml += "</item>";
405                 }
406
407                 xml += "</menu>";
408
409                 if (rNode) {
410                         rNode.containerOpen = false;
411                 }
412
413                 return xml;
414         },
415
416         updateWindowMenu: function() {
417                 this.id_list = [ ];
418                 this.id_uris = [ ];
419
420                 /* Cache preferences */
421                 this.updatePrefs();
422
423                 /* Update some stuff that Firefox does not update regularly */
424                 gEditUIVisible = true;
425                 goUpdateGlobalEditMenuItems();
426                 gEditUIVisible = false;
427                 HistoryMenu.populateUndoSubmenu();
428                 HistoryMenu.populateUndoWindowSubmenu();
429
430                 /* Start walking the XUL menu bar tree */
431                 var menubar = document.getElementById("main-menubar");
432                 var xml = "<menu>";
433                 for (var item=menubar.firstChild; item; item=item.nextSibling) {
434                         if (item.tagName != "menu") continue;
435                         var label = this.createLabelWithAccessKey(item.label, item.accessKey);
436                         xml += "<item id=\"" + item.id + "\" label=\"" +
437                                 this.escapeXml(label) + 
438                                 "\" type=\"d\" >" +
439                                 this.genMenu("/" + item.id, item) + "</item>";
440                 }
441                 xml += "</menu>";
442
443                 /* Send XML to menu server. */
444                 this.serv.updateMenu(xml);
445         },
446
447         /* This will batch updates. */
448         queueUpdateWindowMenu: function() {
449                 if (this.focused && !this.update_timeout) {
450                         this.update_timeout = setTimeout(function(t) {
451                                         t.updateWindowMenu();
452                                         t.update_timeout = 0;
453                                 }, this.batch_interval, this);
454                 };
455         },
456
457         onMenuEvent : function(path) {
458                 var item = this.id_list[path];
459                 if (!item) return; /* Item not found. Maybe popup? */
460                 item.doCommand();
461         },
462
463         onMenuActive : function(path) {
464                 var uri = this.id_uris[path];
465                 if (uri) {
466                         window.XULBrowserWindow.setOverLink(uri, null);
467                 }
468         },
469
470         onMenuInactive : function(path) {
471                 window.XULBrowserWindow.setOverLink("", null);
472         }
473 };
474
475 window.addEventListener("load",
476         function(event) { Globalmenubar.onLoad(event); }, false);
477 window.addEventListener("activate",
478         function(event) { Globalmenubar.onFocus(event); }, false);
479 window.addEventListener("deactivate",
480         function(event) { Globalmenubar.onBlur(event); }, false);
481