|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", |
|
6 "resource:///modules/CustomizableUI.jsm"); |
|
7 XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", |
|
8 "resource:///modules/ScrollbarSampler.jsm"); |
|
9 XPCOMUtils.defineLazyModuleGetter(this, "Promise", |
|
10 "resource://gre/modules/Promise.jsm"); |
|
11 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", |
|
12 "resource://gre/modules/ShortcutUtils.jsm"); |
|
13 /** |
|
14 * Maintains the state and dispatches events for the main menu panel. |
|
15 */ |
|
16 |
|
17 const PanelUI = { |
|
18 /** Panel events that we listen for. **/ |
|
19 get kEvents() ["popupshowing", "popupshown", "popuphiding", "popuphidden"], |
|
20 /** |
|
21 * Used for lazily getting and memoizing elements from the document. Lazy |
|
22 * getters are set in init, and memoizing happens after the first retrieval. |
|
23 */ |
|
24 get kElements() { |
|
25 return { |
|
26 contents: "PanelUI-contents", |
|
27 mainView: "PanelUI-mainView", |
|
28 multiView: "PanelUI-multiView", |
|
29 helpView: "PanelUI-helpView", |
|
30 menuButton: "PanelUI-menu-button", |
|
31 panel: "PanelUI-popup", |
|
32 scroller: "PanelUI-contents-scroller" |
|
33 }; |
|
34 }, |
|
35 |
|
36 _initialized: false, |
|
37 init: function() { |
|
38 for (let [k, v] of Iterator(this.kElements)) { |
|
39 // Need to do fresh let-bindings per iteration |
|
40 let getKey = k; |
|
41 let id = v; |
|
42 this.__defineGetter__(getKey, function() { |
|
43 delete this[getKey]; |
|
44 return this[getKey] = document.getElementById(id); |
|
45 }); |
|
46 } |
|
47 |
|
48 this.menuButton.addEventListener("mousedown", this); |
|
49 this.menuButton.addEventListener("keypress", this); |
|
50 this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this); |
|
51 window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn); |
|
52 CustomizableUI.addListener(this); |
|
53 this._initialized = true; |
|
54 }, |
|
55 |
|
56 _eventListenersAdded: false, |
|
57 _ensureEventListenersAdded: function() { |
|
58 if (this._eventListenersAdded) |
|
59 return; |
|
60 this._addEventListeners(); |
|
61 }, |
|
62 |
|
63 _addEventListeners: function() { |
|
64 for (let event of this.kEvents) { |
|
65 this.panel.addEventListener(event, this); |
|
66 } |
|
67 |
|
68 this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false); |
|
69 this._eventListenersAdded = true; |
|
70 }, |
|
71 |
|
72 uninit: function() { |
|
73 for (let event of this.kEvents) { |
|
74 this.panel.removeEventListener(event, this); |
|
75 } |
|
76 this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow); |
|
77 this.menuButton.removeEventListener("mousedown", this); |
|
78 this.menuButton.removeEventListener("keypress", this); |
|
79 window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn); |
|
80 CustomizableUI.removeListener(this); |
|
81 this._overlayScrollListenerBoundFn = null; |
|
82 }, |
|
83 |
|
84 /** |
|
85 * Customize mode extracts the mainView and puts it somewhere else while the |
|
86 * user customizes. Upon completion, this function can be called to put the |
|
87 * panel back to where it belongs in normal browsing mode. |
|
88 * |
|
89 * @param aMainView |
|
90 * The mainView node to put back into place. |
|
91 */ |
|
92 setMainView: function(aMainView) { |
|
93 this._ensureEventListenersAdded(); |
|
94 this.multiView.setMainView(aMainView); |
|
95 }, |
|
96 |
|
97 /** |
|
98 * Opens the menu panel if it's closed, or closes it if it's |
|
99 * open. |
|
100 * |
|
101 * @param aEvent the event that triggers the toggle. |
|
102 */ |
|
103 toggle: function(aEvent) { |
|
104 // Don't show the panel if the window is in customization mode, |
|
105 // since this button doubles as an exit path for the user in this case. |
|
106 if (document.documentElement.hasAttribute("customizing")) { |
|
107 return; |
|
108 } |
|
109 this._ensureEventListenersAdded(); |
|
110 if (this.panel.state == "open") { |
|
111 this.hide(); |
|
112 } else if (this.panel.state == "closed") { |
|
113 this.show(aEvent); |
|
114 } |
|
115 }, |
|
116 |
|
117 /** |
|
118 * Opens the menu panel. If the event target has a child with the |
|
119 * toolbarbutton-icon attribute, the panel will be anchored on that child. |
|
120 * Otherwise, the panel is anchored on the event target itself. |
|
121 * |
|
122 * @param aEvent the event (if any) that triggers showing the menu. |
|
123 */ |
|
124 show: function(aEvent) { |
|
125 let deferred = Promise.defer(); |
|
126 |
|
127 this.ensureReady().then(() => { |
|
128 if (this.panel.state == "open" || |
|
129 document.documentElement.hasAttribute("customizing")) { |
|
130 deferred.resolve(); |
|
131 return; |
|
132 } |
|
133 |
|
134 let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls"); |
|
135 if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) { |
|
136 updateEditUIVisibility(); |
|
137 } |
|
138 |
|
139 let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); |
|
140 if (personalBookmarksPlacement && |
|
141 personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) { |
|
142 PlacesToolbarHelper.customizeChange(); |
|
143 } |
|
144 |
|
145 let anchor; |
|
146 if (!aEvent || |
|
147 aEvent.type == "command") { |
|
148 anchor = this.menuButton; |
|
149 } else { |
|
150 anchor = aEvent.target; |
|
151 } |
|
152 |
|
153 this.panel.addEventListener("popupshown", function onPopupShown() { |
|
154 this.removeEventListener("popupshown", onPopupShown); |
|
155 // As an optimization for the customize mode transition, we preload |
|
156 // about:customizing in the background once the menu panel is first |
|
157 // shown. |
|
158 gCustomizationTabPreloader.ensurePreloading(); |
|
159 deferred.resolve(); |
|
160 }); |
|
161 |
|
162 let iconAnchor = |
|
163 document.getAnonymousElementByAttribute(anchor, "class", |
|
164 "toolbarbutton-icon"); |
|
165 this.panel.openPopup(iconAnchor || anchor); |
|
166 }); |
|
167 |
|
168 return deferred.promise; |
|
169 }, |
|
170 |
|
171 /** |
|
172 * If the menu panel is being shown, hide it. |
|
173 */ |
|
174 hide: function() { |
|
175 if (document.documentElement.hasAttribute("customizing")) { |
|
176 return; |
|
177 } |
|
178 |
|
179 this.panel.hidePopup(); |
|
180 }, |
|
181 |
|
182 handleEvent: function(aEvent) { |
|
183 switch (aEvent.type) { |
|
184 case "popupshowing": |
|
185 this._adjustLabelsForAutoHyphens(); |
|
186 // Fall through |
|
187 case "popupshown": |
|
188 // Fall through |
|
189 case "popuphiding": |
|
190 // Fall through |
|
191 case "popuphidden": |
|
192 this._updatePanelButton(aEvent.target); |
|
193 break; |
|
194 case "mousedown": |
|
195 if (aEvent.button == 0) |
|
196 this.toggle(aEvent); |
|
197 break; |
|
198 case "keypress": |
|
199 this.toggle(aEvent); |
|
200 break; |
|
201 } |
|
202 }, |
|
203 |
|
204 isReady: function() { |
|
205 return !!this._isReady; |
|
206 }, |
|
207 |
|
208 /** |
|
209 * Registering the menu panel is done lazily for performance reasons. This |
|
210 * method is exposed so that CustomizationMode can force panel-readyness in the |
|
211 * event that customization mode is started before the panel has been opened |
|
212 * by the user. |
|
213 * |
|
214 * @param aCustomizing (optional) set to true if this was called while entering |
|
215 * customization mode. If that's the case, we trust that customization |
|
216 * mode will handle calling beginBatchUpdate and endBatchUpdate. |
|
217 * |
|
218 * @return a Promise that resolves once the panel is ready to roll. |
|
219 */ |
|
220 ensureReady: function(aCustomizing=false) { |
|
221 if (this._readyPromise) { |
|
222 return this._readyPromise; |
|
223 } |
|
224 this._readyPromise = Task.spawn(function() { |
|
225 if (!this._initialized) { |
|
226 let delayedStartupDeferred = Promise.defer(); |
|
227 let delayedStartupObserver = (aSubject, aTopic, aData) => { |
|
228 if (aSubject == window) { |
|
229 Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); |
|
230 delayedStartupDeferred.resolve(); |
|
231 } |
|
232 }; |
|
233 Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); |
|
234 yield delayedStartupDeferred.promise; |
|
235 } |
|
236 |
|
237 this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang", |
|
238 getLocale()); |
|
239 if (!this._scrollWidth) { |
|
240 // In order to properly center the contents of the panel, while ensuring |
|
241 // that we have enough space on either side to show a scrollbar, we have to |
|
242 // do a bit of hackery. In particular, we calculate a new width for the |
|
243 // scroller, based on the system scrollbar width. |
|
244 this._scrollWidth = |
|
245 (yield ScrollbarSampler.getSystemScrollbarWidth()) + "px"; |
|
246 let cstyle = window.getComputedStyle(this.scroller); |
|
247 let widthStr = cstyle.width; |
|
248 // Get the calculated padding on the left and right sides of |
|
249 // the scroller too. We'll use that in our final calculation so |
|
250 // that if a scrollbar appears, we don't have the contents right |
|
251 // up against the edge of the scroller. |
|
252 let paddingLeft = cstyle.paddingLeft; |
|
253 let paddingRight = cstyle.paddingRight; |
|
254 let calcStr = [widthStr, this._scrollWidth, |
|
255 paddingLeft, paddingRight].join(" + "); |
|
256 this.scroller.style.width = "calc(" + calcStr + ")"; |
|
257 } |
|
258 |
|
259 if (aCustomizing) { |
|
260 CustomizableUI.registerMenuPanel(this.contents); |
|
261 } else { |
|
262 this.beginBatchUpdate(); |
|
263 try { |
|
264 CustomizableUI.registerMenuPanel(this.contents); |
|
265 } finally { |
|
266 this.endBatchUpdate(); |
|
267 } |
|
268 } |
|
269 this._updateQuitTooltip(); |
|
270 this.panel.hidden = false; |
|
271 this._isReady = true; |
|
272 }.bind(this)).then(null, Cu.reportError); |
|
273 |
|
274 return this._readyPromise; |
|
275 }, |
|
276 |
|
277 /** |
|
278 * Switch the panel to the main view if it's not already |
|
279 * in that view. |
|
280 */ |
|
281 showMainView: function() { |
|
282 this._ensureEventListenersAdded(); |
|
283 this.multiView.showMainView(); |
|
284 }, |
|
285 |
|
286 /** |
|
287 * Switch the panel to the help view if it's not already |
|
288 * in that view. |
|
289 */ |
|
290 showHelpView: function(aAnchor) { |
|
291 this._ensureEventListenersAdded(); |
|
292 this.multiView.showSubView("PanelUI-helpView", aAnchor); |
|
293 }, |
|
294 |
|
295 /** |
|
296 * Shows a subview in the panel with a given ID. |
|
297 * |
|
298 * @param aViewId the ID of the subview to show. |
|
299 * @param aAnchor the element that spawned the subview. |
|
300 * @param aPlacementArea the CustomizableUI area that aAnchor is in. |
|
301 */ |
|
302 showSubView: function(aViewId, aAnchor, aPlacementArea) { |
|
303 this._ensureEventListenersAdded(); |
|
304 let viewNode = document.getElementById(aViewId); |
|
305 if (!viewNode) { |
|
306 Cu.reportError("Could not show panel subview with id: " + aViewId); |
|
307 return; |
|
308 } |
|
309 |
|
310 if (!aAnchor) { |
|
311 Cu.reportError("Expected an anchor when opening subview with id: " + aViewId); |
|
312 return; |
|
313 } |
|
314 |
|
315 if (aPlacementArea == CustomizableUI.AREA_PANEL) { |
|
316 this.multiView.showSubView(aViewId, aAnchor); |
|
317 } else if (!aAnchor.open) { |
|
318 aAnchor.open = true; |
|
319 // Emit the ViewShowing event so that the widget definition has a chance |
|
320 // to lazily populate the subview with things. |
|
321 let evt = document.createEvent("CustomEvent"); |
|
322 evt.initCustomEvent("ViewShowing", true, true, viewNode); |
|
323 viewNode.dispatchEvent(evt); |
|
324 if (evt.defaultPrevented) { |
|
325 return; |
|
326 } |
|
327 |
|
328 let tempPanel = document.createElement("panel"); |
|
329 tempPanel.setAttribute("type", "arrow"); |
|
330 tempPanel.setAttribute("id", "customizationui-widget-panel"); |
|
331 tempPanel.setAttribute("class", "cui-widget-panel"); |
|
332 tempPanel.setAttribute("context", ""); |
|
333 document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel); |
|
334 // If the view has a footer, set a convenience class on the panel. |
|
335 tempPanel.classList.toggle("cui-widget-panelWithFooter", |
|
336 viewNode.querySelector(".panel-subview-footer")); |
|
337 |
|
338 let multiView = document.createElement("panelmultiview"); |
|
339 multiView.setAttribute("nosubviews", "true"); |
|
340 tempPanel.appendChild(multiView); |
|
341 multiView.setAttribute("mainViewIsSubView", "true"); |
|
342 multiView.setMainView(viewNode); |
|
343 viewNode.classList.add("cui-widget-panelview"); |
|
344 CustomizableUI.addPanelCloseListeners(tempPanel); |
|
345 |
|
346 let panelRemover = function() { |
|
347 tempPanel.removeEventListener("popuphidden", panelRemover); |
|
348 viewNode.classList.remove("cui-widget-panelview"); |
|
349 CustomizableUI.removePanelCloseListeners(tempPanel); |
|
350 let evt = new CustomEvent("ViewHiding", {detail: viewNode}); |
|
351 viewNode.dispatchEvent(evt); |
|
352 aAnchor.open = false; |
|
353 |
|
354 this.multiView.appendChild(viewNode); |
|
355 tempPanel.parentElement.removeChild(tempPanel); |
|
356 }.bind(this); |
|
357 tempPanel.addEventListener("popuphidden", panelRemover); |
|
358 |
|
359 let iconAnchor = |
|
360 document.getAnonymousElementByAttribute(aAnchor, "class", |
|
361 "toolbarbutton-icon"); |
|
362 |
|
363 tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright"); |
|
364 } |
|
365 }, |
|
366 |
|
367 /** |
|
368 * Open a dialog window that allow the user to customize listed character sets. |
|
369 */ |
|
370 onCharsetCustomizeCommand: function() { |
|
371 this.hide(); |
|
372 window.openDialog("chrome://global/content/customizeCharset.xul", |
|
373 "PrefWindow", |
|
374 "chrome,modal=yes,resizable=yes", |
|
375 "browser"); |
|
376 }, |
|
377 |
|
378 onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) { |
|
379 if (aContainer != this.contents) { |
|
380 return; |
|
381 } |
|
382 if (aWasRemoval) { |
|
383 aNode.removeAttribute("auto-hyphens"); |
|
384 } |
|
385 }, |
|
386 |
|
387 onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) { |
|
388 if (aContainer != this.contents) { |
|
389 return; |
|
390 } |
|
391 if (!aIsRemoval && |
|
392 (this.panel.state == "open" || |
|
393 document.documentElement.hasAttribute("customizing"))) { |
|
394 this._adjustLabelsForAutoHyphens(aNode); |
|
395 } |
|
396 }, |
|
397 |
|
398 /** |
|
399 * Signal that we're about to make a lot of changes to the contents of the |
|
400 * panels all at once. For performance, we ignore the mutations. |
|
401 */ |
|
402 beginBatchUpdate: function() { |
|
403 this._ensureEventListenersAdded(); |
|
404 this.multiView.ignoreMutations = true; |
|
405 }, |
|
406 |
|
407 /** |
|
408 * Signal that we're done making bulk changes to the panel. We now pay |
|
409 * attention to mutations. This automatically synchronizes the multiview |
|
410 * container with whichever view is displayed if the panel is open. |
|
411 */ |
|
412 endBatchUpdate: function(aReason) { |
|
413 this._ensureEventListenersAdded(); |
|
414 this.multiView.ignoreMutations = false; |
|
415 }, |
|
416 |
|
417 _adjustLabelsForAutoHyphens: function(aNode) { |
|
418 let toolbarButtons = aNode ? [aNode] : |
|
419 this.contents.querySelectorAll(".toolbarbutton-1"); |
|
420 for (let node of toolbarButtons) { |
|
421 let label = node.getAttribute("label"); |
|
422 if (!label) { |
|
423 continue; |
|
424 } |
|
425 if (label.contains("\u00ad")) { |
|
426 node.setAttribute("auto-hyphens", "off"); |
|
427 } else { |
|
428 node.removeAttribute("auto-hyphens"); |
|
429 } |
|
430 } |
|
431 }, |
|
432 |
|
433 /** |
|
434 * Sets the anchor node into the open or closed state, depending |
|
435 * on the state of the panel. |
|
436 */ |
|
437 _updatePanelButton: function() { |
|
438 this.menuButton.open = this.panel.state == "open" || |
|
439 this.panel.state == "showing"; |
|
440 }, |
|
441 |
|
442 _onHelpViewShow: function(aEvent) { |
|
443 // Call global menu setup function |
|
444 buildHelpMenu(); |
|
445 |
|
446 let helpMenu = document.getElementById("menu_HelpPopup"); |
|
447 let items = this.getElementsByTagName("vbox")[0]; |
|
448 let attrs = ["oncommand", "onclick", "label", "key", "disabled"]; |
|
449 let NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
450 |
|
451 // Remove all buttons from the view |
|
452 while (items.firstChild) { |
|
453 items.removeChild(items.firstChild); |
|
454 } |
|
455 |
|
456 // Add the current set of menuitems of the Help menu to this view |
|
457 let menuItems = Array.prototype.slice.call(helpMenu.getElementsByTagName("menuitem")); |
|
458 let fragment = document.createDocumentFragment(); |
|
459 for (let node of menuItems) { |
|
460 if (node.hidden) |
|
461 continue; |
|
462 let button = document.createElementNS(NSXUL, "toolbarbutton"); |
|
463 // Copy specific attributes from a menuitem of the Help menu |
|
464 for (let attrName of attrs) { |
|
465 if (!node.hasAttribute(attrName)) |
|
466 continue; |
|
467 button.setAttribute(attrName, node.getAttribute(attrName)); |
|
468 } |
|
469 button.setAttribute("class", "subviewbutton"); |
|
470 fragment.appendChild(button); |
|
471 } |
|
472 items.appendChild(fragment); |
|
473 }, |
|
474 |
|
475 _updateQuitTooltip: function() { |
|
476 #ifndef XP_WIN |
|
477 #ifdef XP_MACOSX |
|
478 let tooltipId = "quit-button.tooltiptext.mac"; |
|
479 #else |
|
480 let tooltipId = "quit-button.tooltiptext.linux2"; |
|
481 #endif |
|
482 let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties"); |
|
483 let stringArgs = [brands.GetStringFromName("brandShortName")]; |
|
484 |
|
485 let key = document.getElementById("key_quitApplication"); |
|
486 stringArgs.push(ShortcutUtils.prettifyShortcut(key)); |
|
487 let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs); |
|
488 let quitButton = document.getElementById("PanelUI-quit"); |
|
489 quitButton.setAttribute("tooltiptext", tooltipString); |
|
490 #endif |
|
491 }, |
|
492 |
|
493 _overlayScrollListenerBoundFn: null, |
|
494 _overlayScrollListener: function(aMQL) { |
|
495 ScrollbarSampler.resetSystemScrollbarWidth(); |
|
496 this._scrollWidth = null; |
|
497 }, |
|
498 }; |
|
499 |
|
500 /** |
|
501 * Gets the currently selected locale for display. |
|
502 * @return the selected locale or "en-US" if none is selected |
|
503 */ |
|
504 function getLocale() { |
|
505 const PREF_SELECTED_LOCALE = "general.useragent.locale"; |
|
506 try { |
|
507 let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE, |
|
508 Ci.nsIPrefLocalizedString); |
|
509 if (locale) |
|
510 return locale; |
|
511 } |
|
512 catch (e) { } |
|
513 |
|
514 try { |
|
515 return Services.prefs.getCharPref(PREF_SELECTED_LOCALE); |
|
516 } |
|
517 catch (e) { } |
|
518 |
|
519 return "en-US"; |
|
520 } |
|
521 |