Wed, 31 Dec 2014 07:53:36 +0100
Correct small whitespace inconsistency, lost while renaming variables.
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/. */
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 */
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 },
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 }
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 },
56 _eventListenersAdded: false,
57 _ensureEventListenersAdded: function() {
58 if (this._eventListenersAdded)
59 return;
60 this._addEventListeners();
61 },
63 _addEventListeners: function() {
64 for (let event of this.kEvents) {
65 this.panel.addEventListener(event, this);
66 }
68 this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false);
69 this._eventListenersAdded = true;
70 },
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 },
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 },
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 },
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();
127 this.ensureReady().then(() => {
128 if (this.panel.state == "open" ||
129 document.documentElement.hasAttribute("customizing")) {
130 deferred.resolve();
131 return;
132 }
134 let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
135 if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
136 updateEditUIVisibility();
137 }
139 let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
140 if (personalBookmarksPlacement &&
141 personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
142 PlacesToolbarHelper.customizeChange();
143 }
145 let anchor;
146 if (!aEvent ||
147 aEvent.type == "command") {
148 anchor = this.menuButton;
149 } else {
150 anchor = aEvent.target;
151 }
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 });
162 let iconAnchor =
163 document.getAnonymousElementByAttribute(anchor, "class",
164 "toolbarbutton-icon");
165 this.panel.openPopup(iconAnchor || anchor);
166 });
168 return deferred.promise;
169 },
171 /**
172 * If the menu panel is being shown, hide it.
173 */
174 hide: function() {
175 if (document.documentElement.hasAttribute("customizing")) {
176 return;
177 }
179 this.panel.hidePopup();
180 },
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 },
204 isReady: function() {
205 return !!this._isReady;
206 },
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 }
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 }
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);
274 return this._readyPromise;
275 },
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 },
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 },
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 }
310 if (!aAnchor) {
311 Cu.reportError("Expected an anchor when opening subview with id: " + aViewId);
312 return;
313 }
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 }
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"));
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);
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;
354 this.multiView.appendChild(viewNode);
355 tempPanel.parentElement.removeChild(tempPanel);
356 }.bind(this);
357 tempPanel.addEventListener("popuphidden", panelRemover);
359 let iconAnchor =
360 document.getAnonymousElementByAttribute(aAnchor, "class",
361 "toolbarbutton-icon");
363 tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
364 }
365 },
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 },
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 },
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 },
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 },
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 },
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 },
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 },
442 _onHelpViewShow: function(aEvent) {
443 // Call global menu setup function
444 buildHelpMenu();
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";
451 // Remove all buttons from the view
452 while (items.firstChild) {
453 items.removeChild(items.firstChild);
454 }
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 },
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")];
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 },
493 _overlayScrollListenerBoundFn: null,
494 _overlayScrollListener: function(aMQL) {
495 ScrollbarSampler.resetSystemScrollbarWidth();
496 this._scrollWidth = null;
497 },
498 };
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) { }
514 try {
515 return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
516 }
517 catch (e) { }
519 return "en-US";
520 }