1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/customizableui/content/panelUI.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,521 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", 1.9 + "resource:///modules/CustomizableUI.jsm"); 1.10 +XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", 1.11 + "resource:///modules/ScrollbarSampler.jsm"); 1.12 +XPCOMUtils.defineLazyModuleGetter(this, "Promise", 1.13 + "resource://gre/modules/Promise.jsm"); 1.14 +XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", 1.15 + "resource://gre/modules/ShortcutUtils.jsm"); 1.16 +/** 1.17 + * Maintains the state and dispatches events for the main menu panel. 1.18 + */ 1.19 + 1.20 +const PanelUI = { 1.21 + /** Panel events that we listen for. **/ 1.22 + get kEvents() ["popupshowing", "popupshown", "popuphiding", "popuphidden"], 1.23 + /** 1.24 + * Used for lazily getting and memoizing elements from the document. Lazy 1.25 + * getters are set in init, and memoizing happens after the first retrieval. 1.26 + */ 1.27 + get kElements() { 1.28 + return { 1.29 + contents: "PanelUI-contents", 1.30 + mainView: "PanelUI-mainView", 1.31 + multiView: "PanelUI-multiView", 1.32 + helpView: "PanelUI-helpView", 1.33 + menuButton: "PanelUI-menu-button", 1.34 + panel: "PanelUI-popup", 1.35 + scroller: "PanelUI-contents-scroller" 1.36 + }; 1.37 + }, 1.38 + 1.39 + _initialized: false, 1.40 + init: function() { 1.41 + for (let [k, v] of Iterator(this.kElements)) { 1.42 + // Need to do fresh let-bindings per iteration 1.43 + let getKey = k; 1.44 + let id = v; 1.45 + this.__defineGetter__(getKey, function() { 1.46 + delete this[getKey]; 1.47 + return this[getKey] = document.getElementById(id); 1.48 + }); 1.49 + } 1.50 + 1.51 + this.menuButton.addEventListener("mousedown", this); 1.52 + this.menuButton.addEventListener("keypress", this); 1.53 + this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this); 1.54 + window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn); 1.55 + CustomizableUI.addListener(this); 1.56 + this._initialized = true; 1.57 + }, 1.58 + 1.59 + _eventListenersAdded: false, 1.60 + _ensureEventListenersAdded: function() { 1.61 + if (this._eventListenersAdded) 1.62 + return; 1.63 + this._addEventListeners(); 1.64 + }, 1.65 + 1.66 + _addEventListeners: function() { 1.67 + for (let event of this.kEvents) { 1.68 + this.panel.addEventListener(event, this); 1.69 + } 1.70 + 1.71 + this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false); 1.72 + this._eventListenersAdded = true; 1.73 + }, 1.74 + 1.75 + uninit: function() { 1.76 + for (let event of this.kEvents) { 1.77 + this.panel.removeEventListener(event, this); 1.78 + } 1.79 + this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow); 1.80 + this.menuButton.removeEventListener("mousedown", this); 1.81 + this.menuButton.removeEventListener("keypress", this); 1.82 + window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn); 1.83 + CustomizableUI.removeListener(this); 1.84 + this._overlayScrollListenerBoundFn = null; 1.85 + }, 1.86 + 1.87 + /** 1.88 + * Customize mode extracts the mainView and puts it somewhere else while the 1.89 + * user customizes. Upon completion, this function can be called to put the 1.90 + * panel back to where it belongs in normal browsing mode. 1.91 + * 1.92 + * @param aMainView 1.93 + * The mainView node to put back into place. 1.94 + */ 1.95 + setMainView: function(aMainView) { 1.96 + this._ensureEventListenersAdded(); 1.97 + this.multiView.setMainView(aMainView); 1.98 + }, 1.99 + 1.100 + /** 1.101 + * Opens the menu panel if it's closed, or closes it if it's 1.102 + * open. 1.103 + * 1.104 + * @param aEvent the event that triggers the toggle. 1.105 + */ 1.106 + toggle: function(aEvent) { 1.107 + // Don't show the panel if the window is in customization mode, 1.108 + // since this button doubles as an exit path for the user in this case. 1.109 + if (document.documentElement.hasAttribute("customizing")) { 1.110 + return; 1.111 + } 1.112 + this._ensureEventListenersAdded(); 1.113 + if (this.panel.state == "open") { 1.114 + this.hide(); 1.115 + } else if (this.panel.state == "closed") { 1.116 + this.show(aEvent); 1.117 + } 1.118 + }, 1.119 + 1.120 + /** 1.121 + * Opens the menu panel. If the event target has a child with the 1.122 + * toolbarbutton-icon attribute, the panel will be anchored on that child. 1.123 + * Otherwise, the panel is anchored on the event target itself. 1.124 + * 1.125 + * @param aEvent the event (if any) that triggers showing the menu. 1.126 + */ 1.127 + show: function(aEvent) { 1.128 + let deferred = Promise.defer(); 1.129 + 1.130 + this.ensureReady().then(() => { 1.131 + if (this.panel.state == "open" || 1.132 + document.documentElement.hasAttribute("customizing")) { 1.133 + deferred.resolve(); 1.134 + return; 1.135 + } 1.136 + 1.137 + let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls"); 1.138 + if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) { 1.139 + updateEditUIVisibility(); 1.140 + } 1.141 + 1.142 + let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); 1.143 + if (personalBookmarksPlacement && 1.144 + personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) { 1.145 + PlacesToolbarHelper.customizeChange(); 1.146 + } 1.147 + 1.148 + let anchor; 1.149 + if (!aEvent || 1.150 + aEvent.type == "command") { 1.151 + anchor = this.menuButton; 1.152 + } else { 1.153 + anchor = aEvent.target; 1.154 + } 1.155 + 1.156 + this.panel.addEventListener("popupshown", function onPopupShown() { 1.157 + this.removeEventListener("popupshown", onPopupShown); 1.158 + // As an optimization for the customize mode transition, we preload 1.159 + // about:customizing in the background once the menu panel is first 1.160 + // shown. 1.161 + gCustomizationTabPreloader.ensurePreloading(); 1.162 + deferred.resolve(); 1.163 + }); 1.164 + 1.165 + let iconAnchor = 1.166 + document.getAnonymousElementByAttribute(anchor, "class", 1.167 + "toolbarbutton-icon"); 1.168 + this.panel.openPopup(iconAnchor || anchor); 1.169 + }); 1.170 + 1.171 + return deferred.promise; 1.172 + }, 1.173 + 1.174 + /** 1.175 + * If the menu panel is being shown, hide it. 1.176 + */ 1.177 + hide: function() { 1.178 + if (document.documentElement.hasAttribute("customizing")) { 1.179 + return; 1.180 + } 1.181 + 1.182 + this.panel.hidePopup(); 1.183 + }, 1.184 + 1.185 + handleEvent: function(aEvent) { 1.186 + switch (aEvent.type) { 1.187 + case "popupshowing": 1.188 + this._adjustLabelsForAutoHyphens(); 1.189 + // Fall through 1.190 + case "popupshown": 1.191 + // Fall through 1.192 + case "popuphiding": 1.193 + // Fall through 1.194 + case "popuphidden": 1.195 + this._updatePanelButton(aEvent.target); 1.196 + break; 1.197 + case "mousedown": 1.198 + if (aEvent.button == 0) 1.199 + this.toggle(aEvent); 1.200 + break; 1.201 + case "keypress": 1.202 + this.toggle(aEvent); 1.203 + break; 1.204 + } 1.205 + }, 1.206 + 1.207 + isReady: function() { 1.208 + return !!this._isReady; 1.209 + }, 1.210 + 1.211 + /** 1.212 + * Registering the menu panel is done lazily for performance reasons. This 1.213 + * method is exposed so that CustomizationMode can force panel-readyness in the 1.214 + * event that customization mode is started before the panel has been opened 1.215 + * by the user. 1.216 + * 1.217 + * @param aCustomizing (optional) set to true if this was called while entering 1.218 + * customization mode. If that's the case, we trust that customization 1.219 + * mode will handle calling beginBatchUpdate and endBatchUpdate. 1.220 + * 1.221 + * @return a Promise that resolves once the panel is ready to roll. 1.222 + */ 1.223 + ensureReady: function(aCustomizing=false) { 1.224 + if (this._readyPromise) { 1.225 + return this._readyPromise; 1.226 + } 1.227 + this._readyPromise = Task.spawn(function() { 1.228 + if (!this._initialized) { 1.229 + let delayedStartupDeferred = Promise.defer(); 1.230 + let delayedStartupObserver = (aSubject, aTopic, aData) => { 1.231 + if (aSubject == window) { 1.232 + Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); 1.233 + delayedStartupDeferred.resolve(); 1.234 + } 1.235 + }; 1.236 + Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); 1.237 + yield delayedStartupDeferred.promise; 1.238 + } 1.239 + 1.240 + this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang", 1.241 + getLocale()); 1.242 + if (!this._scrollWidth) { 1.243 + // In order to properly center the contents of the panel, while ensuring 1.244 + // that we have enough space on either side to show a scrollbar, we have to 1.245 + // do a bit of hackery. In particular, we calculate a new width for the 1.246 + // scroller, based on the system scrollbar width. 1.247 + this._scrollWidth = 1.248 + (yield ScrollbarSampler.getSystemScrollbarWidth()) + "px"; 1.249 + let cstyle = window.getComputedStyle(this.scroller); 1.250 + let widthStr = cstyle.width; 1.251 + // Get the calculated padding on the left and right sides of 1.252 + // the scroller too. We'll use that in our final calculation so 1.253 + // that if a scrollbar appears, we don't have the contents right 1.254 + // up against the edge of the scroller. 1.255 + let paddingLeft = cstyle.paddingLeft; 1.256 + let paddingRight = cstyle.paddingRight; 1.257 + let calcStr = [widthStr, this._scrollWidth, 1.258 + paddingLeft, paddingRight].join(" + "); 1.259 + this.scroller.style.width = "calc(" + calcStr + ")"; 1.260 + } 1.261 + 1.262 + if (aCustomizing) { 1.263 + CustomizableUI.registerMenuPanel(this.contents); 1.264 + } else { 1.265 + this.beginBatchUpdate(); 1.266 + try { 1.267 + CustomizableUI.registerMenuPanel(this.contents); 1.268 + } finally { 1.269 + this.endBatchUpdate(); 1.270 + } 1.271 + } 1.272 + this._updateQuitTooltip(); 1.273 + this.panel.hidden = false; 1.274 + this._isReady = true; 1.275 + }.bind(this)).then(null, Cu.reportError); 1.276 + 1.277 + return this._readyPromise; 1.278 + }, 1.279 + 1.280 + /** 1.281 + * Switch the panel to the main view if it's not already 1.282 + * in that view. 1.283 + */ 1.284 + showMainView: function() { 1.285 + this._ensureEventListenersAdded(); 1.286 + this.multiView.showMainView(); 1.287 + }, 1.288 + 1.289 + /** 1.290 + * Switch the panel to the help view if it's not already 1.291 + * in that view. 1.292 + */ 1.293 + showHelpView: function(aAnchor) { 1.294 + this._ensureEventListenersAdded(); 1.295 + this.multiView.showSubView("PanelUI-helpView", aAnchor); 1.296 + }, 1.297 + 1.298 + /** 1.299 + * Shows a subview in the panel with a given ID. 1.300 + * 1.301 + * @param aViewId the ID of the subview to show. 1.302 + * @param aAnchor the element that spawned the subview. 1.303 + * @param aPlacementArea the CustomizableUI area that aAnchor is in. 1.304 + */ 1.305 + showSubView: function(aViewId, aAnchor, aPlacementArea) { 1.306 + this._ensureEventListenersAdded(); 1.307 + let viewNode = document.getElementById(aViewId); 1.308 + if (!viewNode) { 1.309 + Cu.reportError("Could not show panel subview with id: " + aViewId); 1.310 + return; 1.311 + } 1.312 + 1.313 + if (!aAnchor) { 1.314 + Cu.reportError("Expected an anchor when opening subview with id: " + aViewId); 1.315 + return; 1.316 + } 1.317 + 1.318 + if (aPlacementArea == CustomizableUI.AREA_PANEL) { 1.319 + this.multiView.showSubView(aViewId, aAnchor); 1.320 + } else if (!aAnchor.open) { 1.321 + aAnchor.open = true; 1.322 + // Emit the ViewShowing event so that the widget definition has a chance 1.323 + // to lazily populate the subview with things. 1.324 + let evt = document.createEvent("CustomEvent"); 1.325 + evt.initCustomEvent("ViewShowing", true, true, viewNode); 1.326 + viewNode.dispatchEvent(evt); 1.327 + if (evt.defaultPrevented) { 1.328 + return; 1.329 + } 1.330 + 1.331 + let tempPanel = document.createElement("panel"); 1.332 + tempPanel.setAttribute("type", "arrow"); 1.333 + tempPanel.setAttribute("id", "customizationui-widget-panel"); 1.334 + tempPanel.setAttribute("class", "cui-widget-panel"); 1.335 + tempPanel.setAttribute("context", ""); 1.336 + document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel); 1.337 + // If the view has a footer, set a convenience class on the panel. 1.338 + tempPanel.classList.toggle("cui-widget-panelWithFooter", 1.339 + viewNode.querySelector(".panel-subview-footer")); 1.340 + 1.341 + let multiView = document.createElement("panelmultiview"); 1.342 + multiView.setAttribute("nosubviews", "true"); 1.343 + tempPanel.appendChild(multiView); 1.344 + multiView.setAttribute("mainViewIsSubView", "true"); 1.345 + multiView.setMainView(viewNode); 1.346 + viewNode.classList.add("cui-widget-panelview"); 1.347 + CustomizableUI.addPanelCloseListeners(tempPanel); 1.348 + 1.349 + let panelRemover = function() { 1.350 + tempPanel.removeEventListener("popuphidden", panelRemover); 1.351 + viewNode.classList.remove("cui-widget-panelview"); 1.352 + CustomizableUI.removePanelCloseListeners(tempPanel); 1.353 + let evt = new CustomEvent("ViewHiding", {detail: viewNode}); 1.354 + viewNode.dispatchEvent(evt); 1.355 + aAnchor.open = false; 1.356 + 1.357 + this.multiView.appendChild(viewNode); 1.358 + tempPanel.parentElement.removeChild(tempPanel); 1.359 + }.bind(this); 1.360 + tempPanel.addEventListener("popuphidden", panelRemover); 1.361 + 1.362 + let iconAnchor = 1.363 + document.getAnonymousElementByAttribute(aAnchor, "class", 1.364 + "toolbarbutton-icon"); 1.365 + 1.366 + tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright"); 1.367 + } 1.368 + }, 1.369 + 1.370 + /** 1.371 + * Open a dialog window that allow the user to customize listed character sets. 1.372 + */ 1.373 + onCharsetCustomizeCommand: function() { 1.374 + this.hide(); 1.375 + window.openDialog("chrome://global/content/customizeCharset.xul", 1.376 + "PrefWindow", 1.377 + "chrome,modal=yes,resizable=yes", 1.378 + "browser"); 1.379 + }, 1.380 + 1.381 + onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) { 1.382 + if (aContainer != this.contents) { 1.383 + return; 1.384 + } 1.385 + if (aWasRemoval) { 1.386 + aNode.removeAttribute("auto-hyphens"); 1.387 + } 1.388 + }, 1.389 + 1.390 + onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) { 1.391 + if (aContainer != this.contents) { 1.392 + return; 1.393 + } 1.394 + if (!aIsRemoval && 1.395 + (this.panel.state == "open" || 1.396 + document.documentElement.hasAttribute("customizing"))) { 1.397 + this._adjustLabelsForAutoHyphens(aNode); 1.398 + } 1.399 + }, 1.400 + 1.401 + /** 1.402 + * Signal that we're about to make a lot of changes to the contents of the 1.403 + * panels all at once. For performance, we ignore the mutations. 1.404 + */ 1.405 + beginBatchUpdate: function() { 1.406 + this._ensureEventListenersAdded(); 1.407 + this.multiView.ignoreMutations = true; 1.408 + }, 1.409 + 1.410 + /** 1.411 + * Signal that we're done making bulk changes to the panel. We now pay 1.412 + * attention to mutations. This automatically synchronizes the multiview 1.413 + * container with whichever view is displayed if the panel is open. 1.414 + */ 1.415 + endBatchUpdate: function(aReason) { 1.416 + this._ensureEventListenersAdded(); 1.417 + this.multiView.ignoreMutations = false; 1.418 + }, 1.419 + 1.420 + _adjustLabelsForAutoHyphens: function(aNode) { 1.421 + let toolbarButtons = aNode ? [aNode] : 1.422 + this.contents.querySelectorAll(".toolbarbutton-1"); 1.423 + for (let node of toolbarButtons) { 1.424 + let label = node.getAttribute("label"); 1.425 + if (!label) { 1.426 + continue; 1.427 + } 1.428 + if (label.contains("\u00ad")) { 1.429 + node.setAttribute("auto-hyphens", "off"); 1.430 + } else { 1.431 + node.removeAttribute("auto-hyphens"); 1.432 + } 1.433 + } 1.434 + }, 1.435 + 1.436 + /** 1.437 + * Sets the anchor node into the open or closed state, depending 1.438 + * on the state of the panel. 1.439 + */ 1.440 + _updatePanelButton: function() { 1.441 + this.menuButton.open = this.panel.state == "open" || 1.442 + this.panel.state == "showing"; 1.443 + }, 1.444 + 1.445 + _onHelpViewShow: function(aEvent) { 1.446 + // Call global menu setup function 1.447 + buildHelpMenu(); 1.448 + 1.449 + let helpMenu = document.getElementById("menu_HelpPopup"); 1.450 + let items = this.getElementsByTagName("vbox")[0]; 1.451 + let attrs = ["oncommand", "onclick", "label", "key", "disabled"]; 1.452 + let NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.453 + 1.454 + // Remove all buttons from the view 1.455 + while (items.firstChild) { 1.456 + items.removeChild(items.firstChild); 1.457 + } 1.458 + 1.459 + // Add the current set of menuitems of the Help menu to this view 1.460 + let menuItems = Array.prototype.slice.call(helpMenu.getElementsByTagName("menuitem")); 1.461 + let fragment = document.createDocumentFragment(); 1.462 + for (let node of menuItems) { 1.463 + if (node.hidden) 1.464 + continue; 1.465 + let button = document.createElementNS(NSXUL, "toolbarbutton"); 1.466 + // Copy specific attributes from a menuitem of the Help menu 1.467 + for (let attrName of attrs) { 1.468 + if (!node.hasAttribute(attrName)) 1.469 + continue; 1.470 + button.setAttribute(attrName, node.getAttribute(attrName)); 1.471 + } 1.472 + button.setAttribute("class", "subviewbutton"); 1.473 + fragment.appendChild(button); 1.474 + } 1.475 + items.appendChild(fragment); 1.476 + }, 1.477 + 1.478 + _updateQuitTooltip: function() { 1.479 +#ifndef XP_WIN 1.480 +#ifdef XP_MACOSX 1.481 + let tooltipId = "quit-button.tooltiptext.mac"; 1.482 +#else 1.483 + let tooltipId = "quit-button.tooltiptext.linux2"; 1.484 +#endif 1.485 + let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties"); 1.486 + let stringArgs = [brands.GetStringFromName("brandShortName")]; 1.487 + 1.488 + let key = document.getElementById("key_quitApplication"); 1.489 + stringArgs.push(ShortcutUtils.prettifyShortcut(key)); 1.490 + let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs); 1.491 + let quitButton = document.getElementById("PanelUI-quit"); 1.492 + quitButton.setAttribute("tooltiptext", tooltipString); 1.493 +#endif 1.494 + }, 1.495 + 1.496 + _overlayScrollListenerBoundFn: null, 1.497 + _overlayScrollListener: function(aMQL) { 1.498 + ScrollbarSampler.resetSystemScrollbarWidth(); 1.499 + this._scrollWidth = null; 1.500 + }, 1.501 +}; 1.502 + 1.503 +/** 1.504 + * Gets the currently selected locale for display. 1.505 + * @return the selected locale or "en-US" if none is selected 1.506 + */ 1.507 +function getLocale() { 1.508 + const PREF_SELECTED_LOCALE = "general.useragent.locale"; 1.509 + try { 1.510 + let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE, 1.511 + Ci.nsIPrefLocalizedString); 1.512 + if (locale) 1.513 + return locale; 1.514 + } 1.515 + catch (e) { } 1.516 + 1.517 + try { 1.518 + return Services.prefs.getCharPref(PREF_SELECTED_LOCALE); 1.519 + } 1.520 + catch (e) { } 1.521 + 1.522 + return "en-US"; 1.523 +} 1.524 +