diff -r 000000000000 -r 6474c204b198 browser/components/customizableui/src/CustomizeMode.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/customizableui/src/CustomizeMode.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,2004 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["CustomizeMode"]; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +const kPrefCustomizationDebug = "browser.uiCustomization.debug"; +const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation"; +const kPaletteId = "customization-palette"; +const kAboutURI = "about:customizing"; +const kDragDataTypePrefix = "text/toolbarwrapper-id/"; +const kPlaceholderClass = "panel-customization-placeholder"; +const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck"; +const kToolbarVisibilityBtn = "customization-toolbar-visibility-button"; +const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar"; +const kMaxTransitionDurationMs = 2000; + +const kPanelItemContextMenu = "customizationPanelItemContextMenu"; +const kPaletteItemContextMenu = "customizationPaletteItemContextMenu"; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/CustomizableUI.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager", + "resource:///modules/DragPositionManager.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", + "resource:///modules/BrowserUITelemetry.jsm"); + +let gModuleName = "[CustomizeMode]"; +#include logging.js + +let gDisableAnimation = null; + +let gDraggingInToolbars; + +function CustomizeMode(aWindow) { + if (gDisableAnimation === null) { + gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL && + Services.prefs.getBoolPref(kPrefCustomizationAnimation); + } + this.window = aWindow; + this.document = aWindow.document; + this.browser = aWindow.gBrowser; + + // There are two palettes - there's the palette that can be overlayed with + // toolbar items in browser.xul. This is invisible, and never seen by the + // user. Then there's the visible palette, which gets populated and displayed + // to the user when in customizing mode. + this.visiblePalette = this.document.getElementById(kPaletteId); + this.paletteEmptyNotice = this.document.getElementById("customization-empty"); + this.paletteSpacer = this.document.getElementById("customization-spacer"); + this.tipPanel = this.document.getElementById("customization-tipPanel"); +#ifdef CAN_DRAW_IN_TITLEBAR + this._updateTitlebarButton(); + Services.prefs.addObserver(kDrawInTitlebarPref, this, false); + this.window.addEventListener("unload", this); +#endif +}; + +CustomizeMode.prototype = { + _changed: false, + _transitioning: false, + window: null, + document: null, + // areas is used to cache the customizable areas when in customization mode. + areas: null, + // When in customizing mode, we swap out the reference to the invisible + // palette in gNavToolbox.palette for our visiblePalette. This way, for the + // customizing browser window, when widgets are removed from customizable + // areas and added to the palette, they're added to the visible palette. + // _stowedPalette is a reference to the old invisible palette so we can + // restore gNavToolbox.palette to its original state after exiting + // customization mode. + _stowedPalette: null, + _dragOverItem: null, + _customizing: false, + _skipSourceNodeCheck: null, + _mainViewContext: null, + + get panelUIContents() { + return this.document.getElementById("PanelUI-contents"); + }, + + get _handler() { + return this.window.CustomizationHandler; + }, + + uninit: function() { +#ifdef CAN_DRAW_IN_TITLEBAR + Services.prefs.removeObserver(kDrawInTitlebarPref, this); +#endif + }, + + toggle: function() { + if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) { + this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode; + return; + } + if (this._customizing) { + this.exit(); + } else { + this.enter(); + } + }, + + enter: function() { + this._wantToBeInCustomizeMode = true; + + if (this._customizing || this._handler.isEnteringCustomizeMode) { + return; + } + + // Exiting; want to re-enter once we've done that. + if (this._handler.isExitingCustomizeMode) { + LOG("Attempted to enter while we're in the middle of exiting. " + + "We'll exit after we've entered"); + return; + } + + + // We don't need to switch to kAboutURI, or open a new tab at + // kAboutURI if we're already on it. + if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) { + this.window.switchToTabHavingURI(kAboutURI, true, { + skipTabAnimation: true, + }); + return; + } + + let window = this.window; + let document = this.document; + + this._handler.isEnteringCustomizeMode = true; + + // Always disable the reset button at the start of customize mode, it'll be re-enabled + // if necessary when we finish entering: + let resetButton = this.document.getElementById("customization-reset-button"); + resetButton.setAttribute("disabled", "true"); + + Task.spawn(function() { + // We shouldn't start customize mode until after browser-delayed-startup has finished: + if (!this.window.gBrowserInit.delayedStartupFinished) { + let delayedStartupDeferred = Promise.defer(); + let delayedStartupObserver = function(aSubject) { + if (aSubject == this.window) { + Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); + delayedStartupDeferred.resolve(); + } + }.bind(this); + Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); + yield delayedStartupDeferred.promise; + } + + let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn); + let togglableToolbars = window.getTogglableToolbars(); + let bookmarksToolbar = document.getElementById("PersonalToolbar"); + if (togglableToolbars.length == 0) { + toolbarVisibilityBtn.setAttribute("hidden", "true"); + } else { + toolbarVisibilityBtn.removeAttribute("hidden"); + } + + // Disable lightweight themes while in customization mode since + // they don't have large enough images to pad the full browser window. + if (this.document.documentElement._lightweightTheme) + this.document.documentElement._lightweightTheme.disable(); + + CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window); + CustomizableUI.notifyStartCustomizing(this.window); + + // Add a keypress listener to the document so that we can quickly exit + // customization mode when pressing ESC. + document.addEventListener("keypress", this); + + // Same goes for the menu button - if we're customizing, a click on the + // menu button means a quick exit from customization mode. + window.PanelUI.hide(); + window.PanelUI.menuButton.addEventListener("command", this); + window.PanelUI.menuButton.open = true; + window.PanelUI.beginBatchUpdate(); + + // The menu panel is lazy, and registers itself when the popup shows. We + // need to force the menu panel to register itself, or else customization + // is really not going to work. We pass "true" to ensureReady to + // indicate that we're handling calling startBatchUpdate and + // endBatchUpdate. + if (!window.PanelUI.isReady()) { + yield window.PanelUI.ensureReady(true); + } + + // Hide the palette before starting the transition for increased perf. + this.visiblePalette.hidden = true; + this.visiblePalette.removeAttribute("showing"); + + // Disable the button-text fade-out mask + // during the transition for increased perf. + let panelContents = window.PanelUI.contents; + panelContents.setAttribute("customize-transitioning", "true"); + + // Move the mainView in the panel to the holder so that we can see it + // while customizing. + let mainView = window.PanelUI.mainView; + let panelHolder = document.getElementById("customization-panelHolder"); + panelHolder.appendChild(mainView); + + let customizeButton = document.getElementById("PanelUI-customize"); + customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label")); + customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel")); + customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext")); + customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext")); + + this._transitioning = true; + + let customizer = document.getElementById("customization-container"); + customizer.parentNode.selectedPanel = customizer; + customizer.hidden = false; + + yield this._doTransition(true); + + // Let everybody in this window know that we're about to customize. + CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window); + + this._mainViewContext = mainView.getAttribute("context"); + if (this._mainViewContext) { + mainView.removeAttribute("context"); + } + + this._showPanelCustomizationPlaceholders(); + + yield this._wrapToolbarItems(); + this.populatePalette(); + + this._addDragHandlers(this.visiblePalette); + + window.gNavToolbox.addEventListener("toolbarvisibilitychange", this); + + document.getElementById("PanelUI-help").setAttribute("disabled", true); + document.getElementById("PanelUI-quit").setAttribute("disabled", true); + + this._updateResetButton(); + this._updateUndoResetButton(); + + this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL && + Services.prefs.getBoolPref(kSkipSourceNodePref); + + let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])"); + for (let toolbar of customizableToolbars) + toolbar.setAttribute("customizing", true); + + CustomizableUI.addListener(this); + window.PanelUI.endBatchUpdate(); + this._customizing = true; + this._transitioning = false; + + // Show the palette now that the transition has finished. + this.visiblePalette.hidden = false; + window.setTimeout(() => { + // Force layout reflow to ensure the animation runs, + // and make it async so it doesn't affect the timing. + this.visiblePalette.clientTop; + this.visiblePalette.setAttribute("showing", "true"); + }, 0); + this.paletteSpacer.hidden = true; + this._updateEmptyPaletteNotice(); + + this.maybeShowTip(panelHolder); + + this._handler.isEnteringCustomizeMode = false; + panelContents.removeAttribute("customize-transitioning"); + + CustomizableUI.dispatchToolboxEvent("customizationready", {}, window); + this._enableOutlinesTimeout = window.setTimeout(() => { + this.document.getElementById("nav-bar").setAttribute("showoutline", "true"); + this.panelUIContents.setAttribute("showoutline", "true"); + delete this._enableOutlinesTimeout; + }, 0); + + // It's possible that we didn't enter customize mode via the menu panel, + // meaning we didn't kick off about:customizing preloading. If that's + // the case, let's kick it off for the next time we load this mode. + window.gCustomizationTabPreloader.ensurePreloading(); + if (!this._wantToBeInCustomizeMode) { + this.exit(); + } + }.bind(this)).then(null, function(e) { + ERROR(e); + // We should ensure this has been called, and calling it again doesn't hurt: + window.PanelUI.endBatchUpdate(); + this._handler.isEnteringCustomizeMode = false; + }.bind(this)); + }, + + exit: function() { + this._wantToBeInCustomizeMode = false; + + if (!this._customizing || this._handler.isExitingCustomizeMode) { + return; + } + + // Entering; want to exit once we've done that. + if (this._handler.isEnteringCustomizeMode) { + LOG("Attempted to exit while we're in the middle of entering. " + + "We'll exit after we've entered"); + return; + } + + if (this.resetting) { + LOG("Attempted to exit while we're resetting. " + + "We'll exit after resetting has finished."); + return; + } + + this.hideTip(); + + this._handler.isExitingCustomizeMode = true; + + if (this._enableOutlinesTimeout) { + this.window.clearTimeout(this._enableOutlinesTimeout); + } else { + this.document.getElementById("nav-bar").removeAttribute("showoutline"); + this.panelUIContents.removeAttribute("showoutline"); + } + + this._removeExtraToolbarsIfEmpty(); + + CustomizableUI.removeListener(this); + + this.document.removeEventListener("keypress", this); + this.window.PanelUI.menuButton.removeEventListener("command", this); + this.window.PanelUI.menuButton.open = false; + + this.window.PanelUI.beginBatchUpdate(); + + this._removePanelCustomizationPlaceholders(); + + let window = this.window; + let document = this.document; + let documentElement = document.documentElement; + + // Hide the palette before starting the transition for increased perf. + this.paletteSpacer.hidden = false; + this.visiblePalette.hidden = true; + this.visiblePalette.removeAttribute("showing"); + this.paletteEmptyNotice.hidden = true; + + // Disable the button-text fade-out mask + // during the transition for increased perf. + let panelContents = window.PanelUI.contents; + panelContents.setAttribute("customize-transitioning", "true"); + + // Disable the reset and undo reset buttons while transitioning: + let resetButton = this.document.getElementById("customization-reset-button"); + let undoResetButton = this.document.getElementById("customization-undo-reset-button"); + undoResetButton.hidden = resetButton.disabled = true; + + this._transitioning = true; + + Task.spawn(function() { + yield this.depopulatePalette(); + + yield this._doTransition(false); + + let browser = document.getElementById("browser"); + if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) { + let custBrowser = this.browser.selectedBrowser; + if (custBrowser.canGoBack) { + // If there's history to this tab, just go back. + // Note that this throws an exception if the previous document has a + // problematic URL (e.g. about:idontexist) + try { + custBrowser.goBack(); + } catch (ex) { + ERROR(ex); + } + } else { + // If we can't go back, we're removing the about:customization tab. + // We only do this if we're the top window for this window (so not + // a dialog window, for example). + if (window.getTopWin(true) == window) { + let customizationTab = this.browser.selectedTab; + if (this.browser.browsers.length == 1) { + window.BrowserOpenTab(); + } + this.browser.removeTab(customizationTab); + } + } + } + browser.parentNode.selectedPanel = browser; + let customizer = document.getElementById("customization-container"); + customizer.hidden = true; + + window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this); + + DragPositionManager.stop(); + this._removeDragHandlers(this.visiblePalette); + + yield this._unwrapToolbarItems(); + + if (this._changed) { + // XXXmconley: At first, it seems strange to also persist the old way with + // currentset - but this might actually be useful for switching + // to old builds. We might want to keep this around for a little + // bit. + this.persistCurrentSets(); + } + + // And drop all area references. + this.areas = []; + + // Let everybody in this window know that we're starting to + // exit customization mode. + CustomizableUI.dispatchToolboxEvent("customizationending", {}, window); + + window.PanelUI.setMainView(window.PanelUI.mainView); + window.PanelUI.menuButton.disabled = false; + + let customizeButton = document.getElementById("PanelUI-customize"); + customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label")); + customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel")); + customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext")); + customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext")); + + // We have to use setAttribute/removeAttribute here instead of the + // property because the XBL property will be set later, and right + // now we'd be setting an expando, which breaks the XBL property. + document.getElementById("PanelUI-help").removeAttribute("disabled"); + document.getElementById("PanelUI-quit").removeAttribute("disabled"); + + panelContents.removeAttribute("customize-transitioning"); + + // We need to set this._customizing to false before removing the tab + // or the TabSelect event handler will think that we are exiting + // customization mode for a second time. + this._customizing = false; + + let mainView = window.PanelUI.mainView; + if (this._mainViewContext) { + mainView.setAttribute("context", this._mainViewContext); + } + + if (this.document.documentElement._lightweightTheme) + this.document.documentElement._lightweightTheme.enable(); + + let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])"); + for (let toolbar of customizableToolbars) + toolbar.removeAttribute("customizing"); + + this.window.PanelUI.endBatchUpdate(); + this._changed = false; + this._transitioning = false; + this._handler.isExitingCustomizeMode = false; + CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); + CustomizableUI.notifyEndCustomizing(window); + + if (this._wantToBeInCustomizeMode) { + this.enter(); + } + }.bind(this)).then(null, function(e) { + ERROR(e); + // We should ensure this has been called, and calling it again doesn't hurt: + window.PanelUI.endBatchUpdate(); + this._handler.isExitingCustomizeMode = false; + }.bind(this)); + }, + + /** + * The customize mode transition has 3 phases when entering: + * 1) Pre-customization mode + * This is the starting phase of the browser. + * 2) customize-entering + * This phase is a transition, optimized for smoothness. + * 3) customize-entered + * After the transition completes, this phase draws all of the + * expensive detail that isn't necessary during the second phase. + * + * Exiting customization mode has a similar set of phases, but in reverse + * order - customize-entered, customize-exiting, pre-customization mode. + * + * When in the customize-entering, customize-entered, or customize-exiting + * phases, there is a "customizing" attribute set on the main-window to simplify + * excluding certain styles while in any phase of customize mode. + */ + _doTransition: function(aEntering) { + let deferred = Promise.defer(); + let deck = this.document.getElementById("content-deck"); + + let customizeTransitionEnd = function(aEvent) { + if (aEvent != "timedout" && + (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) { + return; + } + this.window.clearTimeout(catchAllTimeout); + // Bug 962677: We let the event loop breathe for before we do the final + // stage of the transition to improve perceived performance. + this.window.setTimeout(function () { + deck.removeEventListener("transitionend", customizeTransitionEnd); + + if (!aEntering) { + this.document.documentElement.removeAttribute("customize-exiting"); + this.document.documentElement.removeAttribute("customizing"); + } else { + this.document.documentElement.setAttribute("customize-entered", true); + this.document.documentElement.removeAttribute("customize-entering"); + } + CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window); + + deferred.resolve(); + }.bind(this), 0); + }.bind(this); + deck.addEventListener("transitionend", customizeTransitionEnd); + + if (gDisableAnimation) { + this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true); + } + if (aEntering) { + this.document.documentElement.setAttribute("customizing", true); + this.document.documentElement.setAttribute("customize-entering", true); + } else { + this.document.documentElement.setAttribute("customize-exiting", true); + this.document.documentElement.removeAttribute("customize-entered"); + } + + let catchAll = () => customizeTransitionEnd("timedout"); + let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs); + return deferred.promise; + }, + + maybeShowTip: function(aAnchor) { + let shown = false; + const kShownPref = "browser.customizemode.tip0.shown"; + try { + shown = Services.prefs.getBoolPref(kShownPref); + } catch (ex) {} + if (shown) + return; + + let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder"); + let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage"); + if (!messageNode.childElementCount) { + // Put the tip contents in the popup. + let bundle = this.document.getElementById("bundle_browser"); + const kLabelClass = "customization-tipPanel-link"; + messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [ + "