/* 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 kDeveditionThemePref = "browser.devedition.theme.enabled"; const kDeveditionButtonPref = "browser.devedition.theme.showCustomizeButton"; const kDeveditionChangedNotification = "devedition-theme-state-changed"; 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"); 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"); 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; 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.paletteSpacer = this.document.getElementById("customization-spacer"); 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"); let deveditionButton = this.document.getElementById("customization-devedition-theme-button"); lwthemeButton.setAttribute("hidden", "true"); deveditionButton.setAttribute("hidden", "true"); } #ifdef CAN_DRAW_IN_TITLEBAR this._updateTitlebarButton(); Services.prefs.addObserver(kDrawInTitlebarPref, this, false); #endif this._updateDevEditionThemeButton(); Services.prefs.addObserver(kDeveditionButtonPref, this, false); Services.obs.addObserver(this, kDeveditionChangedNotification, 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() { #ifdef CAN_DRAW_IN_TITLEBAR Services.prefs.removeObserver(kDrawInTitlebarPref, this); #endif Services.prefs.removeObserver(kDeveditionButtonPref, this); Services.obs.removeObserver(this, kDeveditionChangedNotification); }, 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"); } 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.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); this.removeLWTStyling(); Services.obs.removeObserver(this, "lightweight-theme-window-updated", 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.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) { 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 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 deferred = Promise.defer(); let deck = this.document.getElementById("content-deck"); 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); deferred.resolve(); }); }; 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; }, 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; #ifdef XP_MACOSX let drawingInTitlebar = !docElement.hasAttribute("drawtitle"); let titlebar = this.document.getElementById("titlebar"); if (drawingInTitlebar) { titlebar.style.backgroundImage = headerImageRef; } else { titlebar.style.removeProperty("background-image"); } #endif 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() { #ifdef XP_MACOSX let affectedNodes = ["tab-view-deck", "titlebar"]; #else let affectedNodes = ["tab-view-deck"]; #endif 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", [ "