michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", michael@0: "resource:///modules/CustomizableUI.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", michael@0: "resource:///modules/ScrollbarSampler.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", michael@0: "resource://gre/modules/ShortcutUtils.jsm"); michael@0: /** michael@0: * Maintains the state and dispatches events for the main menu panel. michael@0: */ michael@0: michael@0: const PanelUI = { michael@0: /** Panel events that we listen for. **/ michael@0: get kEvents() ["popupshowing", "popupshown", "popuphiding", "popuphidden"], michael@0: /** michael@0: * Used for lazily getting and memoizing elements from the document. Lazy michael@0: * getters are set in init, and memoizing happens after the first retrieval. michael@0: */ michael@0: get kElements() { michael@0: return { michael@0: contents: "PanelUI-contents", michael@0: mainView: "PanelUI-mainView", michael@0: multiView: "PanelUI-multiView", michael@0: helpView: "PanelUI-helpView", michael@0: menuButton: "PanelUI-menu-button", michael@0: panel: "PanelUI-popup", michael@0: scroller: "PanelUI-contents-scroller" michael@0: }; michael@0: }, michael@0: michael@0: _initialized: false, michael@0: init: function() { michael@0: for (let [k, v] of Iterator(this.kElements)) { michael@0: // Need to do fresh let-bindings per iteration michael@0: let getKey = k; michael@0: let id = v; michael@0: this.__defineGetter__(getKey, function() { michael@0: delete this[getKey]; michael@0: return this[getKey] = document.getElementById(id); michael@0: }); michael@0: } michael@0: michael@0: this.menuButton.addEventListener("mousedown", this); michael@0: this.menuButton.addEventListener("keypress", this); michael@0: this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this); michael@0: window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn); michael@0: CustomizableUI.addListener(this); michael@0: this._initialized = true; michael@0: }, michael@0: michael@0: _eventListenersAdded: false, michael@0: _ensureEventListenersAdded: function() { michael@0: if (this._eventListenersAdded) michael@0: return; michael@0: this._addEventListeners(); michael@0: }, michael@0: michael@0: _addEventListeners: function() { michael@0: for (let event of this.kEvents) { michael@0: this.panel.addEventListener(event, this); michael@0: } michael@0: michael@0: this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false); michael@0: this._eventListenersAdded = true; michael@0: }, michael@0: michael@0: uninit: function() { michael@0: for (let event of this.kEvents) { michael@0: this.panel.removeEventListener(event, this); michael@0: } michael@0: this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow); michael@0: this.menuButton.removeEventListener("mousedown", this); michael@0: this.menuButton.removeEventListener("keypress", this); michael@0: window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn); michael@0: CustomizableUI.removeListener(this); michael@0: this._overlayScrollListenerBoundFn = null; michael@0: }, michael@0: michael@0: /** michael@0: * Customize mode extracts the mainView and puts it somewhere else while the michael@0: * user customizes. Upon completion, this function can be called to put the michael@0: * panel back to where it belongs in normal browsing mode. michael@0: * michael@0: * @param aMainView michael@0: * The mainView node to put back into place. michael@0: */ michael@0: setMainView: function(aMainView) { michael@0: this._ensureEventListenersAdded(); michael@0: this.multiView.setMainView(aMainView); michael@0: }, michael@0: michael@0: /** michael@0: * Opens the menu panel if it's closed, or closes it if it's michael@0: * open. michael@0: * michael@0: * @param aEvent the event that triggers the toggle. michael@0: */ michael@0: toggle: function(aEvent) { michael@0: // Don't show the panel if the window is in customization mode, michael@0: // since this button doubles as an exit path for the user in this case. michael@0: if (document.documentElement.hasAttribute("customizing")) { michael@0: return; michael@0: } michael@0: this._ensureEventListenersAdded(); michael@0: if (this.panel.state == "open") { michael@0: this.hide(); michael@0: } else if (this.panel.state == "closed") { michael@0: this.show(aEvent); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Opens the menu panel. If the event target has a child with the michael@0: * toolbarbutton-icon attribute, the panel will be anchored on that child. michael@0: * Otherwise, the panel is anchored on the event target itself. michael@0: * michael@0: * @param aEvent the event (if any) that triggers showing the menu. michael@0: */ michael@0: show: function(aEvent) { michael@0: let deferred = Promise.defer(); michael@0: michael@0: this.ensureReady().then(() => { michael@0: if (this.panel.state == "open" || michael@0: document.documentElement.hasAttribute("customizing")) { michael@0: deferred.resolve(); michael@0: return; michael@0: } michael@0: michael@0: let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls"); michael@0: if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) { michael@0: updateEditUIVisibility(); michael@0: } michael@0: michael@0: let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); michael@0: if (personalBookmarksPlacement && michael@0: personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) { michael@0: PlacesToolbarHelper.customizeChange(); michael@0: } michael@0: michael@0: let anchor; michael@0: if (!aEvent || michael@0: aEvent.type == "command") { michael@0: anchor = this.menuButton; michael@0: } else { michael@0: anchor = aEvent.target; michael@0: } michael@0: michael@0: this.panel.addEventListener("popupshown", function onPopupShown() { michael@0: this.removeEventListener("popupshown", onPopupShown); michael@0: // As an optimization for the customize mode transition, we preload michael@0: // about:customizing in the background once the menu panel is first michael@0: // shown. michael@0: gCustomizationTabPreloader.ensurePreloading(); michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: let iconAnchor = michael@0: document.getAnonymousElementByAttribute(anchor, "class", michael@0: "toolbarbutton-icon"); michael@0: this.panel.openPopup(iconAnchor || anchor); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * If the menu panel is being shown, hide it. michael@0: */ michael@0: hide: function() { michael@0: if (document.documentElement.hasAttribute("customizing")) { michael@0: return; michael@0: } michael@0: michael@0: this.panel.hidePopup(); michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "popupshowing": michael@0: this._adjustLabelsForAutoHyphens(); michael@0: // Fall through michael@0: case "popupshown": michael@0: // Fall through michael@0: case "popuphiding": michael@0: // Fall through michael@0: case "popuphidden": michael@0: this._updatePanelButton(aEvent.target); michael@0: break; michael@0: case "mousedown": michael@0: if (aEvent.button == 0) michael@0: this.toggle(aEvent); michael@0: break; michael@0: case "keypress": michael@0: this.toggle(aEvent); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: isReady: function() { michael@0: return !!this._isReady; michael@0: }, michael@0: michael@0: /** michael@0: * Registering the menu panel is done lazily for performance reasons. This michael@0: * method is exposed so that CustomizationMode can force panel-readyness in the michael@0: * event that customization mode is started before the panel has been opened michael@0: * by the user. michael@0: * michael@0: * @param aCustomizing (optional) set to true if this was called while entering michael@0: * customization mode. If that's the case, we trust that customization michael@0: * mode will handle calling beginBatchUpdate and endBatchUpdate. michael@0: * michael@0: * @return a Promise that resolves once the panel is ready to roll. michael@0: */ michael@0: ensureReady: function(aCustomizing=false) { michael@0: if (this._readyPromise) { michael@0: return this._readyPromise; michael@0: } michael@0: this._readyPromise = Task.spawn(function() { michael@0: if (!this._initialized) { michael@0: let delayedStartupDeferred = Promise.defer(); michael@0: let delayedStartupObserver = (aSubject, aTopic, aData) => { michael@0: if (aSubject == window) { michael@0: Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); michael@0: delayedStartupDeferred.resolve(); michael@0: } michael@0: }; michael@0: Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); michael@0: yield delayedStartupDeferred.promise; michael@0: } michael@0: michael@0: this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang", michael@0: getLocale()); michael@0: if (!this._scrollWidth) { michael@0: // In order to properly center the contents of the panel, while ensuring michael@0: // that we have enough space on either side to show a scrollbar, we have to michael@0: // do a bit of hackery. In particular, we calculate a new width for the michael@0: // scroller, based on the system scrollbar width. michael@0: this._scrollWidth = michael@0: (yield ScrollbarSampler.getSystemScrollbarWidth()) + "px"; michael@0: let cstyle = window.getComputedStyle(this.scroller); michael@0: let widthStr = cstyle.width; michael@0: // Get the calculated padding on the left and right sides of michael@0: // the scroller too. We'll use that in our final calculation so michael@0: // that if a scrollbar appears, we don't have the contents right michael@0: // up against the edge of the scroller. michael@0: let paddingLeft = cstyle.paddingLeft; michael@0: let paddingRight = cstyle.paddingRight; michael@0: let calcStr = [widthStr, this._scrollWidth, michael@0: paddingLeft, paddingRight].join(" + "); michael@0: this.scroller.style.width = "calc(" + calcStr + ")"; michael@0: } michael@0: michael@0: if (aCustomizing) { michael@0: CustomizableUI.registerMenuPanel(this.contents); michael@0: } else { michael@0: this.beginBatchUpdate(); michael@0: try { michael@0: CustomizableUI.registerMenuPanel(this.contents); michael@0: } finally { michael@0: this.endBatchUpdate(); michael@0: } michael@0: } michael@0: this._updateQuitTooltip(); michael@0: this.panel.hidden = false; michael@0: this._isReady = true; michael@0: }.bind(this)).then(null, Cu.reportError); michael@0: michael@0: return this._readyPromise; michael@0: }, michael@0: michael@0: /** michael@0: * Switch the panel to the main view if it's not already michael@0: * in that view. michael@0: */ michael@0: showMainView: function() { michael@0: this._ensureEventListenersAdded(); michael@0: this.multiView.showMainView(); michael@0: }, michael@0: michael@0: /** michael@0: * Switch the panel to the help view if it's not already michael@0: * in that view. michael@0: */ michael@0: showHelpView: function(aAnchor) { michael@0: this._ensureEventListenersAdded(); michael@0: this.multiView.showSubView("PanelUI-helpView", aAnchor); michael@0: }, michael@0: michael@0: /** michael@0: * Shows a subview in the panel with a given ID. michael@0: * michael@0: * @param aViewId the ID of the subview to show. michael@0: * @param aAnchor the element that spawned the subview. michael@0: * @param aPlacementArea the CustomizableUI area that aAnchor is in. michael@0: */ michael@0: showSubView: function(aViewId, aAnchor, aPlacementArea) { michael@0: this._ensureEventListenersAdded(); michael@0: let viewNode = document.getElementById(aViewId); michael@0: if (!viewNode) { michael@0: Cu.reportError("Could not show panel subview with id: " + aViewId); michael@0: return; michael@0: } michael@0: michael@0: if (!aAnchor) { michael@0: Cu.reportError("Expected an anchor when opening subview with id: " + aViewId); michael@0: return; michael@0: } michael@0: michael@0: if (aPlacementArea == CustomizableUI.AREA_PANEL) { michael@0: this.multiView.showSubView(aViewId, aAnchor); michael@0: } else if (!aAnchor.open) { michael@0: aAnchor.open = true; michael@0: // Emit the ViewShowing event so that the widget definition has a chance michael@0: // to lazily populate the subview with things. michael@0: let evt = document.createEvent("CustomEvent"); michael@0: evt.initCustomEvent("ViewShowing", true, true, viewNode); michael@0: viewNode.dispatchEvent(evt); michael@0: if (evt.defaultPrevented) { michael@0: return; michael@0: } michael@0: michael@0: let tempPanel = document.createElement("panel"); michael@0: tempPanel.setAttribute("type", "arrow"); michael@0: tempPanel.setAttribute("id", "customizationui-widget-panel"); michael@0: tempPanel.setAttribute("class", "cui-widget-panel"); michael@0: tempPanel.setAttribute("context", ""); michael@0: document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel); michael@0: // If the view has a footer, set a convenience class on the panel. michael@0: tempPanel.classList.toggle("cui-widget-panelWithFooter", michael@0: viewNode.querySelector(".panel-subview-footer")); michael@0: michael@0: let multiView = document.createElement("panelmultiview"); michael@0: multiView.setAttribute("nosubviews", "true"); michael@0: tempPanel.appendChild(multiView); michael@0: multiView.setAttribute("mainViewIsSubView", "true"); michael@0: multiView.setMainView(viewNode); michael@0: viewNode.classList.add("cui-widget-panelview"); michael@0: CustomizableUI.addPanelCloseListeners(tempPanel); michael@0: michael@0: let panelRemover = function() { michael@0: tempPanel.removeEventListener("popuphidden", panelRemover); michael@0: viewNode.classList.remove("cui-widget-panelview"); michael@0: CustomizableUI.removePanelCloseListeners(tempPanel); michael@0: let evt = new CustomEvent("ViewHiding", {detail: viewNode}); michael@0: viewNode.dispatchEvent(evt); michael@0: aAnchor.open = false; michael@0: michael@0: this.multiView.appendChild(viewNode); michael@0: tempPanel.parentElement.removeChild(tempPanel); michael@0: }.bind(this); michael@0: tempPanel.addEventListener("popuphidden", panelRemover); michael@0: michael@0: let iconAnchor = michael@0: document.getAnonymousElementByAttribute(aAnchor, "class", michael@0: "toolbarbutton-icon"); michael@0: michael@0: tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Open a dialog window that allow the user to customize listed character sets. michael@0: */ michael@0: onCharsetCustomizeCommand: function() { michael@0: this.hide(); michael@0: window.openDialog("chrome://global/content/customizeCharset.xul", michael@0: "PrefWindow", michael@0: "chrome,modal=yes,resizable=yes", michael@0: "browser"); michael@0: }, michael@0: michael@0: onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) { michael@0: if (aContainer != this.contents) { michael@0: return; michael@0: } michael@0: if (aWasRemoval) { michael@0: aNode.removeAttribute("auto-hyphens"); michael@0: } michael@0: }, michael@0: michael@0: onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) { michael@0: if (aContainer != this.contents) { michael@0: return; michael@0: } michael@0: if (!aIsRemoval && michael@0: (this.panel.state == "open" || michael@0: document.documentElement.hasAttribute("customizing"))) { michael@0: this._adjustLabelsForAutoHyphens(aNode); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Signal that we're about to make a lot of changes to the contents of the michael@0: * panels all at once. For performance, we ignore the mutations. michael@0: */ michael@0: beginBatchUpdate: function() { michael@0: this._ensureEventListenersAdded(); michael@0: this.multiView.ignoreMutations = true; michael@0: }, michael@0: michael@0: /** michael@0: * Signal that we're done making bulk changes to the panel. We now pay michael@0: * attention to mutations. This automatically synchronizes the multiview michael@0: * container with whichever view is displayed if the panel is open. michael@0: */ michael@0: endBatchUpdate: function(aReason) { michael@0: this._ensureEventListenersAdded(); michael@0: this.multiView.ignoreMutations = false; michael@0: }, michael@0: michael@0: _adjustLabelsForAutoHyphens: function(aNode) { michael@0: let toolbarButtons = aNode ? [aNode] : michael@0: this.contents.querySelectorAll(".toolbarbutton-1"); michael@0: for (let node of toolbarButtons) { michael@0: let label = node.getAttribute("label"); michael@0: if (!label) { michael@0: continue; michael@0: } michael@0: if (label.contains("\u00ad")) { michael@0: node.setAttribute("auto-hyphens", "off"); michael@0: } else { michael@0: node.removeAttribute("auto-hyphens"); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets the anchor node into the open or closed state, depending michael@0: * on the state of the panel. michael@0: */ michael@0: _updatePanelButton: function() { michael@0: this.menuButton.open = this.panel.state == "open" || michael@0: this.panel.state == "showing"; michael@0: }, michael@0: michael@0: _onHelpViewShow: function(aEvent) { michael@0: // Call global menu setup function michael@0: buildHelpMenu(); michael@0: michael@0: let helpMenu = document.getElementById("menu_HelpPopup"); michael@0: let items = this.getElementsByTagName("vbox")[0]; michael@0: let attrs = ["oncommand", "onclick", "label", "key", "disabled"]; michael@0: let NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: michael@0: // Remove all buttons from the view michael@0: while (items.firstChild) { michael@0: items.removeChild(items.firstChild); michael@0: } michael@0: michael@0: // Add the current set of menuitems of the Help menu to this view michael@0: let menuItems = Array.prototype.slice.call(helpMenu.getElementsByTagName("menuitem")); michael@0: let fragment = document.createDocumentFragment(); michael@0: for (let node of menuItems) { michael@0: if (node.hidden) michael@0: continue; michael@0: let button = document.createElementNS(NSXUL, "toolbarbutton"); michael@0: // Copy specific attributes from a menuitem of the Help menu michael@0: for (let attrName of attrs) { michael@0: if (!node.hasAttribute(attrName)) michael@0: continue; michael@0: button.setAttribute(attrName, node.getAttribute(attrName)); michael@0: } michael@0: button.setAttribute("class", "subviewbutton"); michael@0: fragment.appendChild(button); michael@0: } michael@0: items.appendChild(fragment); michael@0: }, michael@0: michael@0: _updateQuitTooltip: function() { michael@0: #ifndef XP_WIN michael@0: #ifdef XP_MACOSX michael@0: let tooltipId = "quit-button.tooltiptext.mac"; michael@0: #else michael@0: let tooltipId = "quit-button.tooltiptext.linux2"; michael@0: #endif michael@0: let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties"); michael@0: let stringArgs = [brands.GetStringFromName("brandShortName")]; michael@0: michael@0: let key = document.getElementById("key_quitApplication"); michael@0: stringArgs.push(ShortcutUtils.prettifyShortcut(key)); michael@0: let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs); michael@0: let quitButton = document.getElementById("PanelUI-quit"); michael@0: quitButton.setAttribute("tooltiptext", tooltipString); michael@0: #endif michael@0: }, michael@0: michael@0: _overlayScrollListenerBoundFn: null, michael@0: _overlayScrollListener: function(aMQL) { michael@0: ScrollbarSampler.resetSystemScrollbarWidth(); michael@0: this._scrollWidth = null; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Gets the currently selected locale for display. michael@0: * @return the selected locale or "en-US" if none is selected michael@0: */ michael@0: function getLocale() { michael@0: const PREF_SELECTED_LOCALE = "general.useragent.locale"; michael@0: try { michael@0: let locale = Services.prefs.getComplexValue(PREF_SELECTED_LOCALE, michael@0: Ci.nsIPrefLocalizedString); michael@0: if (locale) michael@0: return locale; michael@0: } michael@0: catch (e) { } michael@0: michael@0: try { michael@0: return Services.prefs.getCharPref(PREF_SELECTED_LOCALE); michael@0: } michael@0: catch (e) { } michael@0: michael@0: return "en-US"; michael@0: } michael@0: