/* 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 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"); Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager", "resource:///modules/DragPositionManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", "resource:///modules/BrowserUITelemetry.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"); let gDebug; XPCOMUtils.defineLazyGetter(this, "log", () => { let scope = {}; Cu.import("resource://gre/modules/Console.jsm", scope); try { gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug); } catch (ex) {} let consoleOptions = { maxLogLevel: gDebug ? "all" : "log", prefix: "CustomizeMode", }; return new scope.ConsoleAPI(consoleOptions); }); var gDisableAnimation = null; var gDraggingInToolbars; var gTab; function closeGlobalTab() { let win = gTab.ownerGlobal; if (win.gBrowser.browsers.length == 1) { win.BrowserOpenTab(); } win.gBrowser.removeTab(gTab); gTab = null; } function unregisterGlobalTab() { gTab.removeEventListener("TabClose", unregisterGlobalTab); gTab.ownerGlobal.removeEventListener("unload", unregisterGlobalTab); gTab.removeAttribute("customizemode"); gTab = null; } 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; this.areas = new Set(); // 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.tipPanel = this.document.getElementById("customization-tipPanel"); if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") { let lwthemeButton = this.document.getElementById("customization-lwtheme-button"); lwthemeButton.setAttribute("hidden", "true"); } if (AppConstants.CAN_DRAW_IN_TITLEBAR) { this._updateTitlebarButton(); Services.prefs.addObserver(kDrawInTitlebarPref, this, false); } this.window.addEventListener("unload", this); } 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() { if (AppConstants.CAN_DRAW_IN_TITLEBAR) { Services.prefs.removeObserver(kDrawInTitlebarPref, this); } }, toggle: function() { if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) { this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode; return; } if (this._customizing) { this.exit(); } else { this.enter(); } }, _updateLWThemeButtonIcon: function() { let lwthemeButton = this.document.getElementById("customization-lwtheme-button"); let lwthemeIcon = this.document.getAnonymousElementByAttribute(lwthemeButton, "class", "button-icon"); lwthemeIcon.style.backgroundImage = LightweightThemeManager.currentTheme ? "url(" + LightweightThemeManager.currentTheme.iconURL + ")" : ""; }, setTab: function(aTab) { if (gTab == aTab) { return; } if (gTab) { closeGlobalTab(); } gTab = aTab; gTab.setAttribute("customizemode", "true"); SessionStore.persistTabAttribute("customizemode"); gTab.linkedBrowser.stop(); let win = gTab.ownerGlobal; win.gBrowser.setTabTitle(gTab); win.gBrowser.setIcon(gTab, "chrome://browser/skin/customizableui/customizeFavicon.ico"); gTab.addEventListener("TabClose", unregisterGlobalTab); win.addEventListener("unload", unregisterGlobalTab); if (gTab.selected) { win.gCustomizeMode.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.debug("Attempted to enter while we're in the middle of exiting. " + "We'll exit after we've entered"); return; } if (!gTab) { this.setTab(this.browser.loadOneTab("about:blank", { inBackground: false, forceNotRemote: true, skipAnimation: true })); return; } if (!gTab.selected) { // This will force another .enter() to be called via the // onlocationchange handler of the tabbrowser, so we return early. gTab.ownerGlobal.gBrowser.selectedTab = gTab; return; } gTab.ownerGlobal.focus(); if (gTab.ownerDocument != this.document) { 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) { yield new Promise(resolve => { let delayedStartupObserver = aSubject => { if (aSubject == this.window) { Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); resolve(); } }; Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); }); } let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn); let togglableToolbars = window.getTogglableToolbars(); if (togglableToolbars.length == 0) { toolbarVisibilityBtn.setAttribute("hidden", "true"); } else { toolbarVisibilityBtn.removeAttribute("hidden"); } this.updateLWTStyling(); 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; this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP); let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])"); for (let toolbar of customizableToolbars) toolbar.setAttribute("customizing", true); yield this._doTransition(true); Services.obs.addObserver(this, "lightweight-theme-window-updated", false); // 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); 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._updateEmptyPaletteNotice(); this._updateLWThemeButtonIcon(); 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); if (!this._wantToBeInCustomizeMode) { this.exit(); } }.bind(this)).then(null, function(e) { log.error("Error entering customize mode", e); // We should ensure this has been called, and calling it again doesn't hurt: window.PanelUI.endBatchUpdate(); this._handler.isEnteringCustomizeMode = false; // Exit customize mode to ensure proper clean-up when entering failed. this.exit(); }.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.debug("Attempted to exit while we're in the middle of entering. " + "We'll exit after we've entered"); return; } if (this.resetting) { log.debug("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; // Hide the palette before starting the transition for increased perf. 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); this.removeLWTStyling(); Services.obs.removeObserver(this, "lightweight-theme-window-updated", false); if (this.browser.selectedTab == gTab) { if (gTab.linkedBrowser.currentURI.spec == "about:blank") { closeGlobalTab(); } else { unregisterGlobalTab(); } } let browser = document.getElementById("browser"); 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.clear(); // 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); } let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])"); for (let toolbar of customizableToolbars) toolbar.removeAttribute("customizing"); this.window.PanelUI.endBatchUpdate(); delete this._lastLightweightTheme; 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) { log.error("Error exiting customize mode", 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 4 phases when entering: * 1) Pre-customization mode * This is the starting phase of the browser. * 2) LWT swapping * This is where we swap some of the lightweight theme styles in order * to make them work in customize mode. We set/unset a customization- * lwtheme attribute iff we're using a lightweight theme. * 3) customize-entering * This phase is a transition, optimized for smoothness. * 4) 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, remove LWT swapping, * 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 deck = this.document.getElementById("content-deck"); let customizeTransitionEndPromise = new Promise(resolve => { let customizeTransitionEnd = (aEvent) => { if (aEvent != "timedout" && (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) { return; } this.window.clearTimeout(catchAllTimeout); // We request an animation frame to do the final stage of the transition // to improve perceived performance. (bug 962677) this.window.requestAnimationFrame(() => { 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); resolve(); }); }; deck.addEventListener("transitionend", customizeTransitionEnd); let catchAll = () => customizeTransitionEnd("timedout"); let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs); }); 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"); } return customizeTransitionEndPromise; }, updateLWTStyling: function(aData) { let docElement = this.document.documentElement; if (!aData) { let lwt = docElement._lightweightTheme; aData = lwt.getData(); } let headerURL = aData && aData.headerURL; if (!headerURL) { this.removeLWTStyling(); return; } let deck = this.document.getElementById("tab-view-deck"); let headerImageRef = this._getHeaderImageRef(aData); docElement.setAttribute("customization-lwtheme", "true"); let toolboxRect = this.window.gNavToolbox.getBoundingClientRect(); let height = toolboxRect.bottom; if (AppConstants.platform == "macosx") { let drawingInTitlebar = !docElement.hasAttribute("drawtitle"); let titlebar = this.document.getElementById("titlebar"); if (drawingInTitlebar) { titlebar.style.backgroundImage = headerImageRef; } else { titlebar.style.removeProperty("background-image"); } } let limitedBG = "-moz-image-rect(" + headerImageRef + ", 0, 100%, " + height + ", 0)"; let ridgeStart = height - 1; let ridgeCenter = (ridgeStart + 1) + "px"; let ridgeEnd = (ridgeStart + 2) + "px"; ridgeStart = ridgeStart + "px"; let ridge = "linear-gradient(to bottom, " + "transparent " + ridgeStart + ", rgba(0,0,0,0.25) " + ridgeStart + ", rgba(0,0,0,0.25) " + ridgeCenter + ", rgba(255,255,255,0.5) " + ridgeCenter + ", rgba(255,255,255,0.5) " + ridgeEnd + ", " + "transparent " + ridgeEnd + ")"; deck.style.backgroundImage = ridge + ", " + limitedBG; /* Remove the background styles from the so we can style it instead. */ docElement.style.removeProperty("background-image"); docElement.style.removeProperty("background-color"); }, removeLWTStyling: function() { let affectedNodes = AppConstants.platform == "macosx" ? ["tab-view-deck", "titlebar"] : ["tab-view-deck"]; for (let id of affectedNodes) { let node = this.document.getElementById(id); node.style.removeProperty("background-image"); } let docElement = this.document.documentElement; docElement.removeAttribute("customization-lwtheme"); let data = docElement._lightweightTheme.getData(); if (data && data.headerURL) { docElement.style.backgroundImage = this._getHeaderImageRef(data); docElement.style.backgroundColor = data.accentcolor || "white"; } }, _getHeaderImageRef: function(aData) { return "url(\"" + aData.headerURL.replace(/"/g, '\\"') + "\")"; }, 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", [ "