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
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: "use strict";
michael@0:
michael@0: this.EXPORTED_SYMBOLS = ["CustomizeMode"];
michael@0:
michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
michael@0:
michael@0: const kPrefCustomizationDebug = "browser.uiCustomization.debug";
michael@0: const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation";
michael@0: const kPaletteId = "customization-palette";
michael@0: const kAboutURI = "about:customizing";
michael@0: const kDragDataTypePrefix = "text/toolbarwrapper-id/";
michael@0: const kPlaceholderClass = "panel-customization-placeholder";
michael@0: const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
michael@0: const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
michael@0: const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
michael@0: const kMaxTransitionDurationMs = 2000;
michael@0:
michael@0: const kPanelItemContextMenu = "customizationPanelItemContextMenu";
michael@0: const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
michael@0:
michael@0: Cu.import("resource://gre/modules/Services.jsm");
michael@0: Cu.import("resource:///modules/CustomizableUI.jsm");
michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0: Cu.import("resource://gre/modules/Task.jsm");
michael@0: Cu.import("resource://gre/modules/Promise.jsm");
michael@0:
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager",
michael@0: "resource:///modules/DragPositionManager.jsm");
michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
michael@0: "resource:///modules/BrowserUITelemetry.jsm");
michael@0:
michael@0: let gModuleName = "[CustomizeMode]";
michael@0: #include logging.js
michael@0:
michael@0: let gDisableAnimation = null;
michael@0:
michael@0: let gDraggingInToolbars;
michael@0:
michael@0: function CustomizeMode(aWindow) {
michael@0: if (gDisableAnimation === null) {
michael@0: gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL &&
michael@0: Services.prefs.getBoolPref(kPrefCustomizationAnimation);
michael@0: }
michael@0: this.window = aWindow;
michael@0: this.document = aWindow.document;
michael@0: this.browser = aWindow.gBrowser;
michael@0:
michael@0: // There are two palettes - there's the palette that can be overlayed with
michael@0: // toolbar items in browser.xul. This is invisible, and never seen by the
michael@0: // user. Then there's the visible palette, which gets populated and displayed
michael@0: // to the user when in customizing mode.
michael@0: this.visiblePalette = this.document.getElementById(kPaletteId);
michael@0: this.paletteEmptyNotice = this.document.getElementById("customization-empty");
michael@0: this.paletteSpacer = this.document.getElementById("customization-spacer");
michael@0: this.tipPanel = this.document.getElementById("customization-tipPanel");
michael@0: #ifdef CAN_DRAW_IN_TITLEBAR
michael@0: this._updateTitlebarButton();
michael@0: Services.prefs.addObserver(kDrawInTitlebarPref, this, false);
michael@0: this.window.addEventListener("unload", this);
michael@0: #endif
michael@0: };
michael@0:
michael@0: CustomizeMode.prototype = {
michael@0: _changed: false,
michael@0: _transitioning: false,
michael@0: window: null,
michael@0: document: null,
michael@0: // areas is used to cache the customizable areas when in customization mode.
michael@0: areas: null,
michael@0: // When in customizing mode, we swap out the reference to the invisible
michael@0: // palette in gNavToolbox.palette for our visiblePalette. This way, for the
michael@0: // customizing browser window, when widgets are removed from customizable
michael@0: // areas and added to the palette, they're added to the visible palette.
michael@0: // _stowedPalette is a reference to the old invisible palette so we can
michael@0: // restore gNavToolbox.palette to its original state after exiting
michael@0: // customization mode.
michael@0: _stowedPalette: null,
michael@0: _dragOverItem: null,
michael@0: _customizing: false,
michael@0: _skipSourceNodeCheck: null,
michael@0: _mainViewContext: null,
michael@0:
michael@0: get panelUIContents() {
michael@0: return this.document.getElementById("PanelUI-contents");
michael@0: },
michael@0:
michael@0: get _handler() {
michael@0: return this.window.CustomizationHandler;
michael@0: },
michael@0:
michael@0: uninit: function() {
michael@0: #ifdef CAN_DRAW_IN_TITLEBAR
michael@0: Services.prefs.removeObserver(kDrawInTitlebarPref, this);
michael@0: #endif
michael@0: },
michael@0:
michael@0: toggle: function() {
michael@0: if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
michael@0: this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
michael@0: return;
michael@0: }
michael@0: if (this._customizing) {
michael@0: this.exit();
michael@0: } else {
michael@0: this.enter();
michael@0: }
michael@0: },
michael@0:
michael@0: enter: function() {
michael@0: this._wantToBeInCustomizeMode = true;
michael@0:
michael@0: if (this._customizing || this._handler.isEnteringCustomizeMode) {
michael@0: return;
michael@0: }
michael@0:
michael@0: // Exiting; want to re-enter once we've done that.
michael@0: if (this._handler.isExitingCustomizeMode) {
michael@0: LOG("Attempted to enter while we're in the middle of exiting. " +
michael@0: "We'll exit after we've entered");
michael@0: return;
michael@0: }
michael@0:
michael@0:
michael@0: // We don't need to switch to kAboutURI, or open a new tab at
michael@0: // kAboutURI if we're already on it.
michael@0: if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) {
michael@0: this.window.switchToTabHavingURI(kAboutURI, true, {
michael@0: skipTabAnimation: true,
michael@0: });
michael@0: return;
michael@0: }
michael@0:
michael@0: let window = this.window;
michael@0: let document = this.document;
michael@0:
michael@0: this._handler.isEnteringCustomizeMode = true;
michael@0:
michael@0: // Always disable the reset button at the start of customize mode, it'll be re-enabled
michael@0: // if necessary when we finish entering:
michael@0: let resetButton = this.document.getElementById("customization-reset-button");
michael@0: resetButton.setAttribute("disabled", "true");
michael@0:
michael@0: Task.spawn(function() {
michael@0: // We shouldn't start customize mode until after browser-delayed-startup has finished:
michael@0: if (!this.window.gBrowserInit.delayedStartupFinished) {
michael@0: let delayedStartupDeferred = Promise.defer();
michael@0: let delayedStartupObserver = function(aSubject) {
michael@0: if (aSubject == this.window) {
michael@0: Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
michael@0: delayedStartupDeferred.resolve();
michael@0: }
michael@0: }.bind(this);
michael@0: Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
michael@0: yield delayedStartupDeferred.promise;
michael@0: }
michael@0:
michael@0: let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
michael@0: let togglableToolbars = window.getTogglableToolbars();
michael@0: let bookmarksToolbar = document.getElementById("PersonalToolbar");
michael@0: if (togglableToolbars.length == 0) {
michael@0: toolbarVisibilityBtn.setAttribute("hidden", "true");
michael@0: } else {
michael@0: toolbarVisibilityBtn.removeAttribute("hidden");
michael@0: }
michael@0:
michael@0: // Disable lightweight themes while in customization mode since
michael@0: // they don't have large enough images to pad the full browser window.
michael@0: if (this.document.documentElement._lightweightTheme)
michael@0: this.document.documentElement._lightweightTheme.disable();
michael@0:
michael@0: CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
michael@0: CustomizableUI.notifyStartCustomizing(this.window);
michael@0:
michael@0: // Add a keypress listener to the document so that we can quickly exit
michael@0: // customization mode when pressing ESC.
michael@0: document.addEventListener("keypress", this);
michael@0:
michael@0: // Same goes for the menu button - if we're customizing, a click on the
michael@0: // menu button means a quick exit from customization mode.
michael@0: window.PanelUI.hide();
michael@0: window.PanelUI.menuButton.addEventListener("command", this);
michael@0: window.PanelUI.menuButton.open = true;
michael@0: window.PanelUI.beginBatchUpdate();
michael@0:
michael@0: // The menu panel is lazy, and registers itself when the popup shows. We
michael@0: // need to force the menu panel to register itself, or else customization
michael@0: // is really not going to work. We pass "true" to ensureReady to
michael@0: // indicate that we're handling calling startBatchUpdate and
michael@0: // endBatchUpdate.
michael@0: if (!window.PanelUI.isReady()) {
michael@0: yield window.PanelUI.ensureReady(true);
michael@0: }
michael@0:
michael@0: // Hide the palette before starting the transition for increased perf.
michael@0: this.visiblePalette.hidden = true;
michael@0: this.visiblePalette.removeAttribute("showing");
michael@0:
michael@0: // Disable the button-text fade-out mask
michael@0: // during the transition for increased perf.
michael@0: let panelContents = window.PanelUI.contents;
michael@0: panelContents.setAttribute("customize-transitioning", "true");
michael@0:
michael@0: // Move the mainView in the panel to the holder so that we can see it
michael@0: // while customizing.
michael@0: let mainView = window.PanelUI.mainView;
michael@0: let panelHolder = document.getElementById("customization-panelHolder");
michael@0: panelHolder.appendChild(mainView);
michael@0:
michael@0: let customizeButton = document.getElementById("PanelUI-customize");
michael@0: customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label"));
michael@0: customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel"));
michael@0: customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext"));
michael@0: customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext"));
michael@0:
michael@0: this._transitioning = true;
michael@0:
michael@0: let customizer = document.getElementById("customization-container");
michael@0: customizer.parentNode.selectedPanel = customizer;
michael@0: customizer.hidden = false;
michael@0:
michael@0: yield this._doTransition(true);
michael@0:
michael@0: // Let everybody in this window know that we're about to customize.
michael@0: CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
michael@0:
michael@0: this._mainViewContext = mainView.getAttribute("context");
michael@0: if (this._mainViewContext) {
michael@0: mainView.removeAttribute("context");
michael@0: }
michael@0:
michael@0: this._showPanelCustomizationPlaceholders();
michael@0:
michael@0: yield this._wrapToolbarItems();
michael@0: this.populatePalette();
michael@0:
michael@0: this._addDragHandlers(this.visiblePalette);
michael@0:
michael@0: window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
michael@0:
michael@0: document.getElementById("PanelUI-help").setAttribute("disabled", true);
michael@0: document.getElementById("PanelUI-quit").setAttribute("disabled", true);
michael@0:
michael@0: this._updateResetButton();
michael@0: this._updateUndoResetButton();
michael@0:
michael@0: this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
michael@0: Services.prefs.getBoolPref(kSkipSourceNodePref);
michael@0:
michael@0: let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
michael@0: for (let toolbar of customizableToolbars)
michael@0: toolbar.setAttribute("customizing", true);
michael@0:
michael@0: CustomizableUI.addListener(this);
michael@0: window.PanelUI.endBatchUpdate();
michael@0: this._customizing = true;
michael@0: this._transitioning = false;
michael@0:
michael@0: // Show the palette now that the transition has finished.
michael@0: this.visiblePalette.hidden = false;
michael@0: window.setTimeout(() => {
michael@0: // Force layout reflow to ensure the animation runs,
michael@0: // and make it async so it doesn't affect the timing.
michael@0: this.visiblePalette.clientTop;
michael@0: this.visiblePalette.setAttribute("showing", "true");
michael@0: }, 0);
michael@0: this.paletteSpacer.hidden = true;
michael@0: this._updateEmptyPaletteNotice();
michael@0:
michael@0: this.maybeShowTip(panelHolder);
michael@0:
michael@0: this._handler.isEnteringCustomizeMode = false;
michael@0: panelContents.removeAttribute("customize-transitioning");
michael@0:
michael@0: CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
michael@0: this._enableOutlinesTimeout = window.setTimeout(() => {
michael@0: this.document.getElementById("nav-bar").setAttribute("showoutline", "true");
michael@0: this.panelUIContents.setAttribute("showoutline", "true");
michael@0: delete this._enableOutlinesTimeout;
michael@0: }, 0);
michael@0:
michael@0: // It's possible that we didn't enter customize mode via the menu panel,
michael@0: // meaning we didn't kick off about:customizing preloading. If that's
michael@0: // the case, let's kick it off for the next time we load this mode.
michael@0: window.gCustomizationTabPreloader.ensurePreloading();
michael@0: if (!this._wantToBeInCustomizeMode) {
michael@0: this.exit();
michael@0: }
michael@0: }.bind(this)).then(null, function(e) {
michael@0: ERROR(e);
michael@0: // We should ensure this has been called, and calling it again doesn't hurt:
michael@0: window.PanelUI.endBatchUpdate();
michael@0: this._handler.isEnteringCustomizeMode = false;
michael@0: }.bind(this));
michael@0: },
michael@0:
michael@0: exit: function() {
michael@0: this._wantToBeInCustomizeMode = false;
michael@0:
michael@0: if (!this._customizing || this._handler.isExitingCustomizeMode) {
michael@0: return;
michael@0: }
michael@0:
michael@0: // Entering; want to exit once we've done that.
michael@0: if (this._handler.isEnteringCustomizeMode) {
michael@0: LOG("Attempted to exit while we're in the middle of entering. " +
michael@0: "We'll exit after we've entered");
michael@0: return;
michael@0: }
michael@0:
michael@0: if (this.resetting) {
michael@0: LOG("Attempted to exit while we're resetting. " +
michael@0: "We'll exit after resetting has finished.");
michael@0: return;
michael@0: }
michael@0:
michael@0: this.hideTip();
michael@0:
michael@0: this._handler.isExitingCustomizeMode = true;
michael@0:
michael@0: if (this._enableOutlinesTimeout) {
michael@0: this.window.clearTimeout(this._enableOutlinesTimeout);
michael@0: } else {
michael@0: this.document.getElementById("nav-bar").removeAttribute("showoutline");
michael@0: this.panelUIContents.removeAttribute("showoutline");
michael@0: }
michael@0:
michael@0: this._removeExtraToolbarsIfEmpty();
michael@0:
michael@0: CustomizableUI.removeListener(this);
michael@0:
michael@0: this.document.removeEventListener("keypress", this);
michael@0: this.window.PanelUI.menuButton.removeEventListener("command", this);
michael@0: this.window.PanelUI.menuButton.open = false;
michael@0:
michael@0: this.window.PanelUI.beginBatchUpdate();
michael@0:
michael@0: this._removePanelCustomizationPlaceholders();
michael@0:
michael@0: let window = this.window;
michael@0: let document = this.document;
michael@0: let documentElement = document.documentElement;
michael@0:
michael@0: // Hide the palette before starting the transition for increased perf.
michael@0: this.paletteSpacer.hidden = false;
michael@0: this.visiblePalette.hidden = true;
michael@0: this.visiblePalette.removeAttribute("showing");
michael@0: this.paletteEmptyNotice.hidden = true;
michael@0:
michael@0: // Disable the button-text fade-out mask
michael@0: // during the transition for increased perf.
michael@0: let panelContents = window.PanelUI.contents;
michael@0: panelContents.setAttribute("customize-transitioning", "true");
michael@0:
michael@0: // Disable the reset and undo reset buttons while transitioning:
michael@0: let resetButton = this.document.getElementById("customization-reset-button");
michael@0: let undoResetButton = this.document.getElementById("customization-undo-reset-button");
michael@0: undoResetButton.hidden = resetButton.disabled = true;
michael@0:
michael@0: this._transitioning = true;
michael@0:
michael@0: Task.spawn(function() {
michael@0: yield this.depopulatePalette();
michael@0:
michael@0: yield this._doTransition(false);
michael@0:
michael@0: let browser = document.getElementById("browser");
michael@0: if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) {
michael@0: let custBrowser = this.browser.selectedBrowser;
michael@0: if (custBrowser.canGoBack) {
michael@0: // If there's history to this tab, just go back.
michael@0: // Note that this throws an exception if the previous document has a
michael@0: // problematic URL (e.g. about:idontexist)
michael@0: try {
michael@0: custBrowser.goBack();
michael@0: } catch (ex) {
michael@0: ERROR(ex);
michael@0: }
michael@0: } else {
michael@0: // If we can't go back, we're removing the about:customization tab.
michael@0: // We only do this if we're the top window for this window (so not
michael@0: // a dialog window, for example).
michael@0: if (window.getTopWin(true) == window) {
michael@0: let customizationTab = this.browser.selectedTab;
michael@0: if (this.browser.browsers.length == 1) {
michael@0: window.BrowserOpenTab();
michael@0: }
michael@0: this.browser.removeTab(customizationTab);
michael@0: }
michael@0: }
michael@0: }
michael@0: browser.parentNode.selectedPanel = browser;
michael@0: let customizer = document.getElementById("customization-container");
michael@0: customizer.hidden = true;
michael@0:
michael@0: window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
michael@0:
michael@0: DragPositionManager.stop();
michael@0: this._removeDragHandlers(this.visiblePalette);
michael@0:
michael@0: yield this._unwrapToolbarItems();
michael@0:
michael@0: if (this._changed) {
michael@0: // XXXmconley: At first, it seems strange to also persist the old way with
michael@0: // currentset - but this might actually be useful for switching
michael@0: // to old builds. We might want to keep this around for a little
michael@0: // bit.
michael@0: this.persistCurrentSets();
michael@0: }
michael@0:
michael@0: // And drop all area references.
michael@0: this.areas = [];
michael@0:
michael@0: // Let everybody in this window know that we're starting to
michael@0: // exit customization mode.
michael@0: CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
michael@0:
michael@0: window.PanelUI.setMainView(window.PanelUI.mainView);
michael@0: window.PanelUI.menuButton.disabled = false;
michael@0:
michael@0: let customizeButton = document.getElementById("PanelUI-customize");
michael@0: customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label"));
michael@0: customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel"));
michael@0: customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext"));
michael@0: customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext"));
michael@0:
michael@0: // We have to use setAttribute/removeAttribute here instead of the
michael@0: // property because the XBL property will be set later, and right
michael@0: // now we'd be setting an expando, which breaks the XBL property.
michael@0: document.getElementById("PanelUI-help").removeAttribute("disabled");
michael@0: document.getElementById("PanelUI-quit").removeAttribute("disabled");
michael@0:
michael@0: panelContents.removeAttribute("customize-transitioning");
michael@0:
michael@0: // We need to set this._customizing to false before removing the tab
michael@0: // or the TabSelect event handler will think that we are exiting
michael@0: // customization mode for a second time.
michael@0: this._customizing = false;
michael@0:
michael@0: let mainView = window.PanelUI.mainView;
michael@0: if (this._mainViewContext) {
michael@0: mainView.setAttribute("context", this._mainViewContext);
michael@0: }
michael@0:
michael@0: if (this.document.documentElement._lightweightTheme)
michael@0: this.document.documentElement._lightweightTheme.enable();
michael@0:
michael@0: let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])");
michael@0: for (let toolbar of customizableToolbars)
michael@0: toolbar.removeAttribute("customizing");
michael@0:
michael@0: this.window.PanelUI.endBatchUpdate();
michael@0: this._changed = false;
michael@0: this._transitioning = false;
michael@0: this._handler.isExitingCustomizeMode = false;
michael@0: CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
michael@0: CustomizableUI.notifyEndCustomizing(window);
michael@0:
michael@0: if (this._wantToBeInCustomizeMode) {
michael@0: this.enter();
michael@0: }
michael@0: }.bind(this)).then(null, function(e) {
michael@0: ERROR(e);
michael@0: // We should ensure this has been called, and calling it again doesn't hurt:
michael@0: window.PanelUI.endBatchUpdate();
michael@0: this._handler.isExitingCustomizeMode = false;
michael@0: }.bind(this));
michael@0: },
michael@0:
michael@0: /**
michael@0: * The customize mode transition has 3 phases when entering:
michael@0: * 1) Pre-customization mode
michael@0: * This is the starting phase of the browser.
michael@0: * 2) customize-entering
michael@0: * This phase is a transition, optimized for smoothness.
michael@0: * 3) customize-entered
michael@0: * After the transition completes, this phase draws all of the
michael@0: * expensive detail that isn't necessary during the second phase.
michael@0: *
michael@0: * Exiting customization mode has a similar set of phases, but in reverse
michael@0: * order - customize-entered, customize-exiting, pre-customization mode.
michael@0: *
michael@0: * When in the customize-entering, customize-entered, or customize-exiting
michael@0: * phases, there is a "customizing" attribute set on the main-window to simplify
michael@0: * excluding certain styles while in any phase of customize mode.
michael@0: */
michael@0: _doTransition: function(aEntering) {
michael@0: let deferred = Promise.defer();
michael@0: let deck = this.document.getElementById("content-deck");
michael@0:
michael@0: let customizeTransitionEnd = function(aEvent) {
michael@0: if (aEvent != "timedout" &&
michael@0: (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
michael@0: return;
michael@0: }
michael@0: this.window.clearTimeout(catchAllTimeout);
michael@0: // Bug 962677: We let the event loop breathe for before we do the final
michael@0: // stage of the transition to improve perceived performance.
michael@0: this.window.setTimeout(function () {
michael@0: deck.removeEventListener("transitionend", customizeTransitionEnd);
michael@0:
michael@0: if (!aEntering) {
michael@0: this.document.documentElement.removeAttribute("customize-exiting");
michael@0: this.document.documentElement.removeAttribute("customizing");
michael@0: } else {
michael@0: this.document.documentElement.setAttribute("customize-entered", true);
michael@0: this.document.documentElement.removeAttribute("customize-entering");
michael@0: }
michael@0: CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
michael@0:
michael@0: deferred.resolve();
michael@0: }.bind(this), 0);
michael@0: }.bind(this);
michael@0: deck.addEventListener("transitionend", customizeTransitionEnd);
michael@0:
michael@0: if (gDisableAnimation) {
michael@0: this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true);
michael@0: }
michael@0: if (aEntering) {
michael@0: this.document.documentElement.setAttribute("customizing", true);
michael@0: this.document.documentElement.setAttribute("customize-entering", true);
michael@0: } else {
michael@0: this.document.documentElement.setAttribute("customize-exiting", true);
michael@0: this.document.documentElement.removeAttribute("customize-entered");
michael@0: }
michael@0:
michael@0: let catchAll = () => customizeTransitionEnd("timedout");
michael@0: let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
michael@0: return deferred.promise;
michael@0: },
michael@0:
michael@0: maybeShowTip: function(aAnchor) {
michael@0: let shown = false;
michael@0: const kShownPref = "browser.customizemode.tip0.shown";
michael@0: try {
michael@0: shown = Services.prefs.getBoolPref(kShownPref);
michael@0: } catch (ex) {}
michael@0: if (shown)
michael@0: return;
michael@0:
michael@0: let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder");
michael@0: let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage");
michael@0: if (!messageNode.childElementCount) {
michael@0: // Put the tip contents in the popup.
michael@0: let bundle = this.document.getElementById("bundle_browser");
michael@0: const kLabelClass = "customization-tipPanel-link";
michael@0: messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [
michael@0: "",
michael@0: this.document.getElementById("bundle_brand").getString("brandShortName"),
michael@0: ""
michael@0: ]);
michael@0:
michael@0: messageNode.querySelector("." + kLabelClass).addEventListener("click", () => {
michael@0: let url = Services.urlFormatter.formatURLPref("browser.customizemode.tip0.learnMoreUrl");
michael@0: let browser = this.browser;
michael@0: browser.selectedTab = browser.addTab(url);
michael@0: this.hideTip();
michael@0: });
michael@0: }
michael@0:
michael@0: this.tipPanel.hidden = false;
michael@0: this.tipPanel.openPopup(anchorNode);
michael@0: Services.prefs.setBoolPref(kShownPref, true);
michael@0: },
michael@0:
michael@0: hideTip: function() {
michael@0: this.tipPanel.hidePopup();
michael@0: },
michael@0:
michael@0: _getCustomizableChildForNode: function(aNode) {
michael@0: // NB: adjusted from _getCustomizableParent to keep that method fast
michael@0: // (it's used during drags), and avoid multiple DOM loops
michael@0: let areas = CustomizableUI.areas;
michael@0: // Caching this length is important because otherwise we'll also iterate
michael@0: // over items we add to the end from within the loop.
michael@0: let numberOfAreas = areas.length;
michael@0: for (let i = 0; i < numberOfAreas; i++) {
michael@0: let area = areas[i];
michael@0: let areaNode = aNode.ownerDocument.getElementById(area);
michael@0: let customizationTarget = areaNode && areaNode.customizationTarget;
michael@0: if (customizationTarget && customizationTarget != areaNode) {
michael@0: areas.push(customizationTarget.id);
michael@0: }
michael@0: let overflowTarget = areaNode.getAttribute("overflowtarget");
michael@0: if (overflowTarget) {
michael@0: areas.push(overflowTarget);
michael@0: }
michael@0: }
michael@0: areas.push(kPaletteId);
michael@0:
michael@0: while (aNode && aNode.parentNode) {
michael@0: let parent = aNode.parentNode;
michael@0: if (areas.indexOf(parent.id) != -1) {
michael@0: return aNode;
michael@0: }
michael@0: aNode = parent;
michael@0: }
michael@0: return null;
michael@0: },
michael@0:
michael@0: addToToolbar: function(aNode) {
michael@0: aNode = this._getCustomizableChildForNode(aNode);
michael@0: if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
michael@0: aNode = aNode.firstChild;
michael@0: }
michael@0: CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR);
michael@0: if (!this._customizing) {
michael@0: CustomizableUI.dispatchToolboxEvent("customizationchange");
michael@0: }
michael@0: },
michael@0:
michael@0: addToPanel: function(aNode) {
michael@0: aNode = this._getCustomizableChildForNode(aNode);
michael@0: if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
michael@0: aNode = aNode.firstChild;
michael@0: }
michael@0: CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_PANEL);
michael@0: if (!this._customizing) {
michael@0: CustomizableUI.dispatchToolboxEvent("customizationchange");
michael@0: }
michael@0: },
michael@0:
michael@0: removeFromArea: function(aNode) {
michael@0: aNode = this._getCustomizableChildForNode(aNode);
michael@0: if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
michael@0: aNode = aNode.firstChild;
michael@0: }
michael@0: CustomizableUI.removeWidgetFromArea(aNode.id);
michael@0: if (!this._customizing) {
michael@0: CustomizableUI.dispatchToolboxEvent("customizationchange");
michael@0: }
michael@0: },
michael@0:
michael@0: populatePalette: function() {
michael@0: let fragment = this.document.createDocumentFragment();
michael@0: let toolboxPalette = this.window.gNavToolbox.palette;
michael@0:
michael@0: try {
michael@0: let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
michael@0: for (let widget of unusedWidgets) {
michael@0: let paletteItem = this.makePaletteItem(widget, "palette");
michael@0: fragment.appendChild(paletteItem);
michael@0: }
michael@0:
michael@0: this.visiblePalette.appendChild(fragment);
michael@0: this._stowedPalette = this.window.gNavToolbox.palette;
michael@0: this.window.gNavToolbox.palette = this.visiblePalette;
michael@0: } catch (ex) {
michael@0: ERROR(ex);
michael@0: }
michael@0: },
michael@0:
michael@0: //XXXunf Maybe this should use -moz-element instead of wrapping the node?
michael@0: // Would ensure no weird interactions/event handling from original node,
michael@0: // and makes it possible to put this in a lazy-loaded iframe/real tab
michael@0: // while still getting rid of the need for overlays.
michael@0: makePaletteItem: function(aWidget, aPlace) {
michael@0: let widgetNode = aWidget.forWindow(this.window).node;
michael@0: let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
michael@0: wrapper.appendChild(widgetNode);
michael@0: return wrapper;
michael@0: },
michael@0:
michael@0: depopulatePalette: function() {
michael@0: return Task.spawn(function() {
michael@0: this.visiblePalette.hidden = true;
michael@0: let paletteChild = this.visiblePalette.firstChild;
michael@0: let nextChild;
michael@0: while (paletteChild) {
michael@0: nextChild = paletteChild.nextElementSibling;
michael@0: let provider = CustomizableUI.getWidget(paletteChild.id).provider;
michael@0: if (provider == CustomizableUI.PROVIDER_XUL) {
michael@0: let unwrappedPaletteItem =
michael@0: yield this.deferredUnwrapToolbarItem(paletteChild);
michael@0: this._stowedPalette.appendChild(unwrappedPaletteItem);
michael@0: } else if (provider == CustomizableUI.PROVIDER_API) {
michael@0: //XXXunf Currently this doesn't destroy the (now unused) node. It would
michael@0: // be good to do so, but we need to keep strong refs to it in
michael@0: // CustomizableUI (can't iterate of WeakMaps), and there's the
michael@0: // question of what behavior wrappers should have if consumers
michael@0: // keep hold of them.
michael@0: //widget.destroyInstance(widgetNode);
michael@0: } else if (provider == CustomizableUI.PROVIDER_SPECIAL) {
michael@0: this.visiblePalette.removeChild(paletteChild);
michael@0: }
michael@0:
michael@0: paletteChild = nextChild;
michael@0: }
michael@0: this.visiblePalette.hidden = false;
michael@0: this.window.gNavToolbox.palette = this._stowedPalette;
michael@0: }.bind(this)).then(null, ERROR);
michael@0: },
michael@0:
michael@0: isCustomizableItem: function(aNode) {
michael@0: return aNode.localName == "toolbarbutton" ||
michael@0: aNode.localName == "toolbaritem" ||
michael@0: aNode.localName == "toolbarseparator" ||
michael@0: aNode.localName == "toolbarspring" ||
michael@0: aNode.localName == "toolbarspacer";
michael@0: },
michael@0:
michael@0: isWrappedToolbarItem: function(aNode) {
michael@0: return aNode.localName == "toolbarpaletteitem";
michael@0: },
michael@0:
michael@0: deferredWrapToolbarItem: function(aNode, aPlace) {
michael@0: let deferred = Promise.defer();
michael@0:
michael@0: dispatchFunction(function() {
michael@0: let wrapper = this.wrapToolbarItem(aNode, aPlace);
michael@0: deferred.resolve(wrapper);
michael@0: }.bind(this))
michael@0:
michael@0: return deferred.promise;
michael@0: },
michael@0:
michael@0: wrapToolbarItem: function(aNode, aPlace) {
michael@0: if (!this.isCustomizableItem(aNode)) {
michael@0: return aNode;
michael@0: }
michael@0: let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
michael@0:
michael@0: // It's possible that this toolbar node is "mid-flight" and doesn't have
michael@0: // a parent, in which case we skip replacing it. This can happen if a
michael@0: // toolbar item has been dragged into the palette. In that case, we tell
michael@0: // CustomizableUI to remove the widget from its area before putting the
michael@0: // widget in the palette - so the node will have no parent.
michael@0: if (aNode.parentNode) {
michael@0: aNode = aNode.parentNode.replaceChild(wrapper, aNode);
michael@0: }
michael@0: wrapper.appendChild(aNode);
michael@0: return wrapper;
michael@0: },
michael@0:
michael@0: createOrUpdateWrapper: function(aNode, aPlace, aIsUpdate) {
michael@0: let wrapper;
michael@0: if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") {
michael@0: wrapper = aNode.parentNode;
michael@0: aPlace = wrapper.getAttribute("place");
michael@0: } else {
michael@0: wrapper = this.document.createElement("toolbarpaletteitem");
michael@0: // "place" is used by toolkit to add the toolbarpaletteitem-palette
michael@0: // binding to a toolbarpaletteitem, which gives it a label node for when
michael@0: // it's sitting in the palette.
michael@0: wrapper.setAttribute("place", aPlace);
michael@0: }
michael@0:
michael@0:
michael@0: // Ensure the wrapped item doesn't look like it's in any special state, and
michael@0: // can't be interactved with when in the customization palette.
michael@0: if (aNode.hasAttribute("command")) {
michael@0: wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
michael@0: aNode.removeAttribute("command");
michael@0: }
michael@0:
michael@0: if (aNode.hasAttribute("observes")) {
michael@0: wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
michael@0: aNode.removeAttribute("observes");
michael@0: }
michael@0:
michael@0: if (aNode.getAttribute("checked") == "true") {
michael@0: wrapper.setAttribute("itemchecked", "true");
michael@0: aNode.removeAttribute("checked");
michael@0: }
michael@0:
michael@0: if (aNode.hasAttribute("id")) {
michael@0: wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
michael@0: }
michael@0:
michael@0: if (aNode.hasAttribute("label")) {
michael@0: wrapper.setAttribute("title", aNode.getAttribute("label"));
michael@0: } else if (aNode.hasAttribute("title")) {
michael@0: wrapper.setAttribute("title", aNode.getAttribute("title"));
michael@0: }
michael@0:
michael@0: if (aNode.hasAttribute("flex")) {
michael@0: wrapper.setAttribute("flex", aNode.getAttribute("flex"));
michael@0: }
michael@0:
michael@0: if (aPlace == "panel") {
michael@0: if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
michael@0: wrapper.setAttribute("haswideitem", "true");
michael@0: } else if (wrapper.hasAttribute("haswideitem")) {
michael@0: wrapper.removeAttribute("haswideitem");
michael@0: }
michael@0: }
michael@0:
michael@0: let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
michael@0: wrapper.setAttribute("removable", removable);
michael@0:
michael@0: let contextMenuAttrName = aNode.getAttribute("context") ? "context" :
michael@0: aNode.getAttribute("contextmenu") ? "contextmenu" : "";
michael@0: let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
michael@0: let contextMenuForPlace = aPlace == "panel" ?
michael@0: kPanelItemContextMenu :
michael@0: kPaletteItemContextMenu;
michael@0: if (aPlace != "toolbar") {
michael@0: wrapper.setAttribute("context", contextMenuForPlace);
michael@0: }
michael@0: // Only keep track of the menu if it is non-default.
michael@0: if (currentContextMenu &&
michael@0: currentContextMenu != contextMenuForPlace) {
michael@0: aNode.setAttribute("wrapped-context", currentContextMenu);
michael@0: aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName)
michael@0: aNode.removeAttribute(contextMenuAttrName);
michael@0: } else if (currentContextMenu == contextMenuForPlace) {
michael@0: aNode.removeAttribute(contextMenuAttrName);
michael@0: }
michael@0:
michael@0: // Only add listeners for newly created wrappers:
michael@0: if (!aIsUpdate) {
michael@0: wrapper.addEventListener("mousedown", this);
michael@0: wrapper.addEventListener("mouseup", this);
michael@0: }
michael@0:
michael@0: return wrapper;
michael@0: },
michael@0:
michael@0: deferredUnwrapToolbarItem: function(aWrapper) {
michael@0: let deferred = Promise.defer();
michael@0: dispatchFunction(function() {
michael@0: let item = null;
michael@0: try {
michael@0: item = this.unwrapToolbarItem(aWrapper);
michael@0: } catch (ex) {
michael@0: Cu.reportError(ex);
michael@0: }
michael@0: deferred.resolve(item);
michael@0: }.bind(this));
michael@0: return deferred.promise;
michael@0: },
michael@0:
michael@0: unwrapToolbarItem: function(aWrapper) {
michael@0: if (aWrapper.nodeName != "toolbarpaletteitem") {
michael@0: return aWrapper;
michael@0: }
michael@0: aWrapper.removeEventListener("mousedown", this);
michael@0: aWrapper.removeEventListener("mouseup", this);
michael@0:
michael@0: let place = aWrapper.getAttribute("place");
michael@0:
michael@0: let toolbarItem = aWrapper.firstChild;
michael@0: if (!toolbarItem) {
michael@0: ERROR("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id);
michael@0: aWrapper.remove();
michael@0: return null;
michael@0: }
michael@0:
michael@0: if (aWrapper.hasAttribute("itemobserves")) {
michael@0: toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves"));
michael@0: }
michael@0:
michael@0: if (aWrapper.hasAttribute("itemchecked")) {
michael@0: toolbarItem.checked = true;
michael@0: }
michael@0:
michael@0: if (aWrapper.hasAttribute("itemcommand")) {
michael@0: let commandID = aWrapper.getAttribute("itemcommand");
michael@0: toolbarItem.setAttribute("command", commandID);
michael@0:
michael@0: //XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
michael@0: let command = this.document.getElementById(commandID);
michael@0: if (command && command.hasAttribute("disabled")) {
michael@0: toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
michael@0: }
michael@0: }
michael@0:
michael@0: let wrappedContext = toolbarItem.getAttribute("wrapped-context");
michael@0: if (wrappedContext) {
michael@0: let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
michael@0: toolbarItem.setAttribute(contextAttrName, wrappedContext);
michael@0: toolbarItem.removeAttribute("wrapped-contextAttrName");
michael@0: toolbarItem.removeAttribute("wrapped-context");
michael@0: } else if (place == "panel") {
michael@0: toolbarItem.setAttribute("context", kPanelItemContextMenu);
michael@0: }
michael@0:
michael@0: if (aWrapper.parentNode) {
michael@0: aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
michael@0: }
michael@0: return toolbarItem;
michael@0: },
michael@0:
michael@0: _wrapToolbarItems: function() {
michael@0: let window = this.window;
michael@0: // Add drag-and-drop event handlers to all of the customizable areas.
michael@0: return Task.spawn(function() {
michael@0: this.areas = [];
michael@0: for (let area of CustomizableUI.areas) {
michael@0: let target = CustomizableUI.getCustomizeTargetForArea(area, window);
michael@0: this._addDragHandlers(target);
michael@0: for (let child of target.children) {
michael@0: if (this.isCustomizableItem(child)) {
michael@0: yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
michael@0: }
michael@0: }
michael@0: this.areas.push(target);
michael@0: }
michael@0: }.bind(this)).then(null, ERROR);
michael@0: },
michael@0:
michael@0: _addDragHandlers: function(aTarget) {
michael@0: aTarget.addEventListener("dragstart", this, true);
michael@0: aTarget.addEventListener("dragover", this, true);
michael@0: aTarget.addEventListener("dragexit", this, true);
michael@0: aTarget.addEventListener("drop", this, true);
michael@0: aTarget.addEventListener("dragend", this, true);
michael@0: },
michael@0:
michael@0: _wrapItemsInArea: function(target) {
michael@0: for (let child of target.children) {
michael@0: if (this.isCustomizableItem(child)) {
michael@0: this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: _removeDragHandlers: function(aTarget) {
michael@0: aTarget.removeEventListener("dragstart", this, true);
michael@0: aTarget.removeEventListener("dragover", this, true);
michael@0: aTarget.removeEventListener("dragexit", this, true);
michael@0: aTarget.removeEventListener("drop", this, true);
michael@0: aTarget.removeEventListener("dragend", this, true);
michael@0: },
michael@0:
michael@0: _unwrapItemsInArea: function(target) {
michael@0: for (let toolbarItem of target.children) {
michael@0: if (this.isWrappedToolbarItem(toolbarItem)) {
michael@0: this.unwrapToolbarItem(toolbarItem);
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: _unwrapToolbarItems: function() {
michael@0: return Task.spawn(function() {
michael@0: for (let target of this.areas) {
michael@0: for (let toolbarItem of target.children) {
michael@0: if (this.isWrappedToolbarItem(toolbarItem)) {
michael@0: yield this.deferredUnwrapToolbarItem(toolbarItem);
michael@0: }
michael@0: }
michael@0: this._removeDragHandlers(target);
michael@0: }
michael@0: }.bind(this)).then(null, ERROR);
michael@0: },
michael@0:
michael@0: _removeExtraToolbarsIfEmpty: function() {
michael@0: let toolbox = this.window.gNavToolbox;
michael@0: for (let child of toolbox.children) {
michael@0: if (child.hasAttribute("customindex")) {
michael@0: let placements = CustomizableUI.getWidgetIdsInArea(child.id);
michael@0: if (!placements.length) {
michael@0: CustomizableUI.removeExtraToolbar(child.id);
michael@0: }
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: persistCurrentSets: function(aSetBeforePersisting) {
michael@0: let document = this.document;
michael@0: let toolbars = document.querySelectorAll("toolbar[customizable='true'][currentset]");
michael@0: for (let toolbar of toolbars) {
michael@0: if (aSetBeforePersisting) {
michael@0: let set = toolbar.currentSet;
michael@0: toolbar.setAttribute("currentset", set);
michael@0: }
michael@0: // Persist the currentset attribute directly on hardcoded toolbars.
michael@0: document.persist(toolbar.id, "currentset");
michael@0: }
michael@0: },
michael@0:
michael@0: reset: function() {
michael@0: this.resetting = true;
michael@0: // Disable the reset button temporarily while resetting:
michael@0: let btn = this.document.getElementById("customization-reset-button");
michael@0: BrowserUITelemetry.countCustomizationEvent("reset");
michael@0: btn.disabled = true;
michael@0: return Task.spawn(function() {
michael@0: this._removePanelCustomizationPlaceholders();
michael@0: yield this.depopulatePalette();
michael@0: yield this._unwrapToolbarItems();
michael@0:
michael@0: CustomizableUI.reset();
michael@0:
michael@0: yield this._wrapToolbarItems();
michael@0: this.populatePalette();
michael@0:
michael@0: this.persistCurrentSets(true);
michael@0:
michael@0: this._updateResetButton();
michael@0: this._updateUndoResetButton();
michael@0: this._updateEmptyPaletteNotice();
michael@0: this._showPanelCustomizationPlaceholders();
michael@0: this.resetting = false;
michael@0: if (!this._wantToBeInCustomizeMode) {
michael@0: this.exit();
michael@0: }
michael@0: }.bind(this)).then(null, ERROR);
michael@0: },
michael@0:
michael@0: undoReset: function() {
michael@0: this.resetting = true;
michael@0:
michael@0: return Task.spawn(function() {
michael@0: this._removePanelCustomizationPlaceholders();
michael@0: yield this.depopulatePalette();
michael@0: yield this._unwrapToolbarItems();
michael@0:
michael@0: CustomizableUI.undoReset();
michael@0:
michael@0: yield this._wrapToolbarItems();
michael@0: this.populatePalette();
michael@0:
michael@0: this.persistCurrentSets(true);
michael@0:
michael@0: this._updateResetButton();
michael@0: this._updateUndoResetButton();
michael@0: this._updateEmptyPaletteNotice();
michael@0: this.resetting = false;
michael@0: }.bind(this)).then(null, ERROR);
michael@0: },
michael@0:
michael@0: _onToolbarVisibilityChange: function(aEvent) {
michael@0: let toolbar = aEvent.target;
michael@0: if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
michael@0: toolbar.setAttribute("customizing", "true");
michael@0: } else {
michael@0: toolbar.removeAttribute("customizing");
michael@0: }
michael@0: this._onUIChange();
michael@0: },
michael@0:
michael@0: onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
michael@0: this._onUIChange();
michael@0: },
michael@0:
michael@0: onWidgetAdded: function(aWidgetId, aArea, aPosition) {
michael@0: this._onUIChange();
michael@0: },
michael@0:
michael@0: onWidgetRemoved: function(aWidgetId, aArea) {
michael@0: this._onUIChange();
michael@0: },
michael@0:
michael@0: onWidgetBeforeDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
michael@0: if (aContainer.ownerDocument.defaultView != this.window || this.resetting) {
michael@0: return;
michael@0: }
michael@0: if (aContainer.id == CustomizableUI.AREA_PANEL) {
michael@0: this._removePanelCustomizationPlaceholders();
michael@0: }
michael@0: // If we get called for widgets that aren't in the window yet, they might not have
michael@0: // a parentNode at all.
michael@0: if (aNodeToChange.parentNode) {
michael@0: this.unwrapToolbarItem(aNodeToChange.parentNode);
michael@0: }
michael@0: if (aSecondaryNode) {
michael@0: this.unwrapToolbarItem(aSecondaryNode.parentNode);
michael@0: }
michael@0: },
michael@0:
michael@0: onWidgetAfterDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
michael@0: if (aContainer.ownerDocument.defaultView != this.window || this.resetting) {
michael@0: return;
michael@0: }
michael@0: // If the node is still attached to the container, wrap it again:
michael@0: if (aNodeToChange.parentNode) {
michael@0: let place = CustomizableUI.getPlaceForItem(aNodeToChange);
michael@0: this.wrapToolbarItem(aNodeToChange, place);
michael@0: if (aSecondaryNode) {
michael@0: this.wrapToolbarItem(aSecondaryNode, place);
michael@0: }
michael@0: } else {
michael@0: // If not, it got removed.
michael@0:
michael@0: // If an API-based widget is removed while customizing, append it to the palette.
michael@0: // The _applyDrop code itself will take care of positioning it correctly, if
michael@0: // applicable. We need the code to be here so removing widgets using CustomizableUI's
michael@0: // API also does the right thing (and adds it to the palette)
michael@0: let widgetId = aNodeToChange.id;
michael@0: let widget = CustomizableUI.getWidget(widgetId);
michael@0: if (widget.provider == CustomizableUI.PROVIDER_API) {
michael@0: let paletteItem = this.makePaletteItem(widget, "palette");
michael@0: this.visiblePalette.appendChild(paletteItem);
michael@0: }
michael@0: }
michael@0: if (aContainer.id == CustomizableUI.AREA_PANEL) {
michael@0: this._showPanelCustomizationPlaceholders();
michael@0: }
michael@0: },
michael@0:
michael@0: onWidgetDestroyed: function(aWidgetId) {
michael@0: let wrapper = this.document.getElementById("wrapper-" + aWidgetId);
michael@0: if (wrapper) {
michael@0: let wasInPanel = wrapper.parentNode == this.panelUIContents;
michael@0: wrapper.remove();
michael@0: if (wasInPanel) {
michael@0: this._showPanelCustomizationPlaceholders();
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: onWidgetAfterCreation: function(aWidgetId, aArea) {
michael@0: // If the node was added to an area, we would have gotten an onWidgetAdded notification,
michael@0: // plus associated DOM change notifications, so only do stuff for the palette:
michael@0: if (!aArea) {
michael@0: let widgetNode = this.document.getElementById(aWidgetId);
michael@0: if (widgetNode) {
michael@0: this.wrapToolbarItem(widgetNode, "palette");
michael@0: } else {
michael@0: let widget = CustomizableUI.getWidget(aWidgetId);
michael@0: this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: onAreaNodeRegistered: function(aArea, aContainer) {
michael@0: if (aContainer.ownerDocument == this.document) {
michael@0: this._wrapItemsInArea(aContainer);
michael@0: this._addDragHandlers(aContainer);
michael@0: DragPositionManager.add(this.window, aArea, aContainer);
michael@0: this.areas.push(aContainer);
michael@0: }
michael@0: },
michael@0:
michael@0: onAreaNodeUnregistered: function(aArea, aContainer, aReason) {
michael@0: if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) {
michael@0: this._unwrapItemsInArea(aContainer);
michael@0: this._removeDragHandlers(aContainer);
michael@0: DragPositionManager.remove(this.window, aArea, aContainer);
michael@0: let index = this.areas.indexOf(aContainer);
michael@0: this.areas.splice(index, 1);
michael@0: }
michael@0: },
michael@0:
michael@0: _onUIChange: function() {
michael@0: this._changed = true;
michael@0: if (!this.resetting) {
michael@0: this._updateResetButton();
michael@0: this._updateUndoResetButton();
michael@0: this._updateEmptyPaletteNotice();
michael@0: }
michael@0: CustomizableUI.dispatchToolboxEvent("customizationchange");
michael@0: },
michael@0:
michael@0: _updateEmptyPaletteNotice: function() {
michael@0: let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
michael@0: this.paletteEmptyNotice.hidden = !!paletteItems.length;
michael@0: },
michael@0:
michael@0: _updateResetButton: function() {
michael@0: let btn = this.document.getElementById("customization-reset-button");
michael@0: btn.disabled = CustomizableUI.inDefaultState;
michael@0: },
michael@0:
michael@0: _updateUndoResetButton: function() {
michael@0: let undoResetButton = this.document.getElementById("customization-undo-reset-button");
michael@0: undoResetButton.hidden = !CustomizableUI.canUndoReset;
michael@0: },
michael@0:
michael@0: handleEvent: function(aEvent) {
michael@0: switch(aEvent.type) {
michael@0: case "toolbarvisibilitychange":
michael@0: this._onToolbarVisibilityChange(aEvent);
michael@0: break;
michael@0: case "dragstart":
michael@0: this._onDragStart(aEvent);
michael@0: break;
michael@0: case "dragover":
michael@0: this._onDragOver(aEvent);
michael@0: break;
michael@0: case "drop":
michael@0: this._onDragDrop(aEvent);
michael@0: break;
michael@0: case "dragexit":
michael@0: this._onDragExit(aEvent);
michael@0: break;
michael@0: case "dragend":
michael@0: this._onDragEnd(aEvent);
michael@0: break;
michael@0: case "command":
michael@0: if (aEvent.originalTarget == this.window.PanelUI.menuButton) {
michael@0: this.exit();
michael@0: aEvent.preventDefault();
michael@0: }
michael@0: break;
michael@0: case "mousedown":
michael@0: this._onMouseDown(aEvent);
michael@0: break;
michael@0: case "mouseup":
michael@0: this._onMouseUp(aEvent);
michael@0: break;
michael@0: case "keypress":
michael@0: if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
michael@0: this.exit();
michael@0: }
michael@0: break;
michael@0: #ifdef CAN_DRAW_IN_TITLEBAR
michael@0: case "unload":
michael@0: this.uninit();
michael@0: break;
michael@0: #endif
michael@0: }
michael@0: },
michael@0:
michael@0: #ifdef CAN_DRAW_IN_TITLEBAR
michael@0: observe: function(aSubject, aTopic, aData) {
michael@0: switch (aTopic) {
michael@0: case "nsPref:changed":
michael@0: this._updateResetButton();
michael@0: this._updateTitlebarButton();
michael@0: this._updateUndoResetButton();
michael@0: break;
michael@0: }
michael@0: },
michael@0:
michael@0: _updateTitlebarButton: function() {
michael@0: let drawInTitlebar = true;
michael@0: try {
michael@0: drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref);
michael@0: } catch (ex) { }
michael@0: let button = this.document.getElementById("customization-titlebar-visibility-button");
michael@0: // Drawing in the titlebar means 'hiding' the titlebar:
michael@0: if (drawInTitlebar) {
michael@0: button.removeAttribute("checked");
michael@0: } else {
michael@0: button.setAttribute("checked", "true");
michael@0: }
michael@0: },
michael@0:
michael@0: toggleTitlebar: function(aShouldShowTitlebar) {
michael@0: // Drawing in the titlebar means not showing the titlebar, hence the negation:
michael@0: Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
michael@0: },
michael@0: #endif
michael@0:
michael@0: _onDragStart: function(aEvent) {
michael@0: __dumpDragData(aEvent);
michael@0: let item = aEvent.target;
michael@0: while (item && item.localName != "toolbarpaletteitem") {
michael@0: if (item.localName == "toolbar") {
michael@0: return;
michael@0: }
michael@0: item = item.parentNode;
michael@0: }
michael@0:
michael@0: let draggedItem = item.firstChild;
michael@0: let placeForItem = CustomizableUI.getPlaceForItem(item);
michael@0: let isRemovable = placeForItem == "palette" ||
michael@0: CustomizableUI.isWidgetRemovable(draggedItem);
michael@0: if (item.classList.contains(kPlaceholderClass) || !isRemovable) {
michael@0: return;
michael@0: }
michael@0:
michael@0: let dt = aEvent.dataTransfer;
michael@0: let documentId = aEvent.target.ownerDocument.documentElement.id;
michael@0: let isInToolbar = placeForItem == "toolbar";
michael@0:
michael@0: dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
michael@0: dt.effectAllowed = "move";
michael@0:
michael@0: let itemRect = draggedItem.getBoundingClientRect();
michael@0: let itemCenter = {x: itemRect.left + itemRect.width / 2,
michael@0: y: itemRect.top + itemRect.height / 2};
michael@0: this._dragOffset = {x: aEvent.clientX - itemCenter.x,
michael@0: y: aEvent.clientY - itemCenter.y};
michael@0:
michael@0: gDraggingInToolbars = new Set();
michael@0:
michael@0: // Hack needed so that the dragimage will still show the
michael@0: // item as it appeared before it was hidden.
michael@0: this._initializeDragAfterMove = function() {
michael@0: // For automated tests, we sometimes start exiting customization mode
michael@0: // before this fires, which leaves us with placeholders inserted after
michael@0: // we've exited. So we need to check that we are indeed customizing.
michael@0: if (this._customizing && !this._transitioning) {
michael@0: item.hidden = true;
michael@0: this._showPanelCustomizationPlaceholders();
michael@0: DragPositionManager.start(this.window);
michael@0: if (item.nextSibling) {
michael@0: this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar);
michael@0: this._dragOverItem = item.nextSibling;
michael@0: } else if (isInToolbar && item.previousSibling) {
michael@0: this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar);
michael@0: this._dragOverItem = item.previousSibling;
michael@0: }
michael@0: }
michael@0: this._initializeDragAfterMove = null;
michael@0: this.window.clearTimeout(this._dragInitializeTimeout);
michael@0: }.bind(this);
michael@0: this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
michael@0: },
michael@0:
michael@0: _onDragOver: function(aEvent) {
michael@0: if (this._isUnwantedDragDrop(aEvent)) {
michael@0: return;
michael@0: }
michael@0: if (this._initializeDragAfterMove) {
michael@0: this._initializeDragAfterMove();
michael@0: }
michael@0:
michael@0: __dumpDragData(aEvent);
michael@0:
michael@0: let document = aEvent.target.ownerDocument;
michael@0: let documentId = document.documentElement.id;
michael@0: if (!aEvent.dataTransfer.mozTypesAt(0)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: let draggedItemId =
michael@0: aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
michael@0: let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
michael@0: let targetArea = this._getCustomizableParent(aEvent.currentTarget);
michael@0: let originArea = this._getCustomizableParent(draggedWrapper);
michael@0:
michael@0: // Do nothing if the target or origin are not customizable.
michael@0: if (!targetArea || !originArea) {
michael@0: return;
michael@0: }
michael@0:
michael@0: // Do nothing if the widget is not allowed to be removed.
michael@0: if (targetArea.id == kPaletteId &&
michael@0: !CustomizableUI.isWidgetRemovable(draggedItemId)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: // Do nothing if the widget is not allowed to move to the target area.
michael@0: if (targetArea.id != kPaletteId &&
michael@0: !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: let targetIsToolbar = CustomizableUI.getAreaType(targetArea.id) == "toolbar";
michael@0: let targetNode = this._getDragOverNode(aEvent, targetArea, targetIsToolbar, draggedItemId);
michael@0:
michael@0: // We need to determine the place that the widget is being dropped in
michael@0: // the target.
michael@0: let dragOverItem, dragValue;
michael@0: if (targetNode == targetArea.customizationTarget) {
michael@0: // We'll assume if the user is dragging directly over the target, that
michael@0: // they're attempting to append a child to that target.
michael@0: dragOverItem = (targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
michael@0: targetNode.lastChild) || targetNode;
michael@0: dragValue = "after";
michael@0: } else {
michael@0: let targetParent = targetNode.parentNode;
michael@0: let position = Array.indexOf(targetParent.children, targetNode);
michael@0: if (position == -1) {
michael@0: dragOverItem = targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
michael@0: targetParent.lastChild;
michael@0: dragValue = "after";
michael@0: } else {
michael@0: dragOverItem = targetParent.children[position];
michael@0: if (!targetIsToolbar) {
michael@0: dragValue = "before";
michael@0: } else {
michael@0: dragOverItem = this._findVisiblePreviousSiblingNode(targetParent.children[position]);
michael@0: // Check if the aDraggedItem is hovered past the first half of dragOverItem
michael@0: let window = dragOverItem.ownerDocument.defaultView;
michael@0: let direction = window.getComputedStyle(dragOverItem, null).direction;
michael@0: let itemRect = dragOverItem.getBoundingClientRect();
michael@0: let dropTargetCenter = itemRect.left + (itemRect.width / 2);
michael@0: let existingDir = dragOverItem.getAttribute("dragover");
michael@0: if ((existingDir == "before") == (direction == "ltr")) {
michael@0: dropTargetCenter += (parseInt(dragOverItem.style.borderLeftWidth) || 0) / 2;
michael@0: } else {
michael@0: dropTargetCenter -= (parseInt(dragOverItem.style.borderRightWidth) || 0) / 2;
michael@0: }
michael@0: let before = direction == "ltr" ? aEvent.clientX < dropTargetCenter : aEvent.clientX > dropTargetCenter;
michael@0: dragValue = before ? "before" : "after";
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: if (this._dragOverItem && dragOverItem != this._dragOverItem) {
michael@0: this._cancelDragActive(this._dragOverItem, dragOverItem);
michael@0: }
michael@0:
michael@0: if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) {
michael@0: if (dragOverItem != targetArea.customizationTarget) {
michael@0: this._setDragActive(dragOverItem, dragValue, draggedItemId, targetIsToolbar);
michael@0: } else if (targetIsToolbar) {
michael@0: this._updateToolbarCustomizationOutline(this.window, targetArea);
michael@0: }
michael@0: this._dragOverItem = dragOverItem;
michael@0: }
michael@0:
michael@0: aEvent.preventDefault();
michael@0: aEvent.stopPropagation();
michael@0: },
michael@0:
michael@0: _onDragDrop: function(aEvent) {
michael@0: if (this._isUnwantedDragDrop(aEvent)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: __dumpDragData(aEvent);
michael@0: this._initializeDragAfterMove = null;
michael@0: this.window.clearTimeout(this._dragInitializeTimeout);
michael@0:
michael@0: let targetArea = this._getCustomizableParent(aEvent.currentTarget);
michael@0: let document = aEvent.target.ownerDocument;
michael@0: let documentId = document.documentElement.id;
michael@0: let draggedItemId =
michael@0: aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
michael@0: let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
michael@0: let originArea = this._getCustomizableParent(draggedWrapper);
michael@0: if (this._dragSizeMap) {
michael@0: this._dragSizeMap.clear();
michael@0: }
michael@0: // Do nothing if the target area or origin area are not customizable.
michael@0: if (!targetArea || !originArea) {
michael@0: return;
michael@0: }
michael@0: let targetNode = this._dragOverItem;
michael@0: let dropDir = targetNode.getAttribute("dragover");
michael@0: // Need to insert *after* this node if we promised the user that:
michael@0: if (targetNode != targetArea && dropDir == "after") {
michael@0: if (targetNode.nextSibling) {
michael@0: targetNode = targetNode.nextSibling;
michael@0: } else {
michael@0: targetNode = targetArea;
michael@0: }
michael@0: }
michael@0: // If the target node is a placeholder, get its sibling as the real target.
michael@0: while (targetNode.classList.contains(kPlaceholderClass) && targetNode.nextSibling) {
michael@0: targetNode = targetNode.nextSibling;
michael@0: }
michael@0: if (targetNode.tagName == "toolbarpaletteitem") {
michael@0: targetNode = targetNode.firstChild;
michael@0: }
michael@0:
michael@0: this._cancelDragActive(this._dragOverItem, null, true);
michael@0: this._removePanelCustomizationPlaceholders();
michael@0:
michael@0: try {
michael@0: this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode);
michael@0: } catch (ex) {
michael@0: ERROR(ex, ex.stack);
michael@0: }
michael@0:
michael@0: this._showPanelCustomizationPlaceholders();
michael@0: },
michael@0:
michael@0: _applyDrop: function(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
michael@0: let document = aEvent.target.ownerDocument;
michael@0: let draggedItem = document.getElementById(aDraggedItemId);
michael@0: draggedItem.hidden = false;
michael@0: draggedItem.removeAttribute("mousedown");
michael@0:
michael@0: // Do nothing if the target was dropped onto itself (ie, no change in area
michael@0: // or position).
michael@0: if (draggedItem == aTargetNode) {
michael@0: return;
michael@0: }
michael@0:
michael@0: // Is the target area the customization palette?
michael@0: if (aTargetArea.id == kPaletteId) {
michael@0: // Did we drag from outside the palette?
michael@0: if (aOriginArea.id !== kPaletteId) {
michael@0: if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: CustomizableUI.removeWidgetFromArea(aDraggedItemId);
michael@0: BrowserUITelemetry.countCustomizationEvent("remove");
michael@0: // Special widgets are removed outright, we can return here:
michael@0: if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
michael@0: return;
michael@0: }
michael@0: }
michael@0: draggedItem = draggedItem.parentNode;
michael@0:
michael@0: // If the target node is the palette itself, just append
michael@0: if (aTargetNode == this.visiblePalette) {
michael@0: this.visiblePalette.appendChild(draggedItem);
michael@0: } else {
michael@0: // The items in the palette are wrapped, so we need the target node's parent here:
michael@0: this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
michael@0: }
michael@0: if (aOriginArea.id !== kPaletteId) {
michael@0: // The dragend event already fires when the item moves within the palette.
michael@0: this._onDragEnd(aEvent);
michael@0: }
michael@0: return;
michael@0: }
michael@0:
michael@0: if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: // Skipintoolbarset items won't really be moved:
michael@0: if (draggedItem.getAttribute("skipintoolbarset") == "true") {
michael@0: // These items should never leave their area:
michael@0: if (aTargetArea != aOriginArea) {
michael@0: return;
michael@0: }
michael@0: let place = draggedItem.parentNode.getAttribute("place");
michael@0: this.unwrapToolbarItem(draggedItem.parentNode);
michael@0: if (aTargetNode == aTargetArea.customizationTarget) {
michael@0: aTargetArea.customizationTarget.appendChild(draggedItem);
michael@0: } else {
michael@0: this.unwrapToolbarItem(aTargetNode.parentNode);
michael@0: aTargetArea.customizationTarget.insertBefore(draggedItem, aTargetNode);
michael@0: this.wrapToolbarItem(aTargetNode, place);
michael@0: }
michael@0: this.wrapToolbarItem(draggedItem, place);
michael@0: BrowserUITelemetry.countCustomizationEvent("move");
michael@0: return;
michael@0: }
michael@0:
michael@0: // Is the target the customization area itself? If so, we just add the
michael@0: // widget to the end of the area.
michael@0: if (aTargetNode == aTargetArea.customizationTarget) {
michael@0: CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
michael@0: // For the purposes of BrowserUITelemetry, we consider both moving a widget
michael@0: // within the same area, and adding a widget from one area to another area
michael@0: // as a "move". An "add" is only when we move an item from the palette into
michael@0: // an area.
michael@0: let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
michael@0: BrowserUITelemetry.countCustomizationEvent(custEventType);
michael@0: this._onDragEnd(aEvent);
michael@0: return;
michael@0: }
michael@0:
michael@0: // We need to determine the place that the widget is being dropped in
michael@0: // the target.
michael@0: let placement;
michael@0: let itemForPlacement = aTargetNode;
michael@0: // Skip the skipintoolbarset items when determining the place of the item:
michael@0: while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
michael@0: itemForPlacement.parentNode &&
michael@0: itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") {
michael@0: itemForPlacement = itemForPlacement.parentNode.nextSibling;
michael@0: if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") {
michael@0: itemForPlacement = itemForPlacement.firstChild;
michael@0: }
michael@0: }
michael@0: if (itemForPlacement && !itemForPlacement.classList.contains(kPlaceholderClass)) {
michael@0: let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ?
michael@0: itemForPlacement.firstChild && itemForPlacement.firstChild.id :
michael@0: itemForPlacement.id;
michael@0: placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
michael@0: }
michael@0: if (!placement) {
michael@0: LOG("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className);
michael@0: }
michael@0: let position = placement ? placement.position : null;
michael@0:
michael@0: // Is the target area the same as the origin? Since we've already handled
michael@0: // the possibility that the target is the customization palette, we know
michael@0: // that the widget is moving within a customizable area.
michael@0: if (aTargetArea == aOriginArea) {
michael@0: CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position);
michael@0: } else {
michael@0: CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
michael@0: }
michael@0:
michael@0: this._onDragEnd(aEvent);
michael@0:
michael@0: // For BrowserUITelemetry, an "add" is only when we move an item from the palette
michael@0: // into an area. Otherwise, it's a move.
michael@0: let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
michael@0: BrowserUITelemetry.countCustomizationEvent(custEventType);
michael@0:
michael@0: // If we dropped onto a skipintoolbarset item, manually correct the drop location:
michael@0: if (aTargetNode != itemForPlacement) {
michael@0: let draggedWrapper = draggedItem.parentNode;
michael@0: let container = draggedWrapper.parentNode;
michael@0: container.insertBefore(draggedWrapper, aTargetNode.parentNode);
michael@0: }
michael@0: },
michael@0:
michael@0: _onDragExit: function(aEvent) {
michael@0: if (this._isUnwantedDragDrop(aEvent)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: __dumpDragData(aEvent);
michael@0:
michael@0: // When leaving customization areas, cancel the drag on the last dragover item
michael@0: // We've attached the listener to areas, so aEvent.currentTarget will be the area.
michael@0: // We don't care about dragexit events fired on descendants of the area,
michael@0: // so we check that the event's target is the same as the area to which the listener
michael@0: // was attached.
michael@0: if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
michael@0: this._cancelDragActive(this._dragOverItem);
michael@0: this._dragOverItem = null;
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
michael@0: */
michael@0: _onDragEnd: function(aEvent) {
michael@0: if (this._isUnwantedDragDrop(aEvent)) {
michael@0: return;
michael@0: }
michael@0: this._initializeDragAfterMove = null;
michael@0: this.window.clearTimeout(this._dragInitializeTimeout);
michael@0: __dumpDragData(aEvent, "_onDragEnd");
michael@0:
michael@0: let document = aEvent.target.ownerDocument;
michael@0: document.documentElement.removeAttribute("customizing-movingItem");
michael@0:
michael@0: let documentId = document.documentElement.id;
michael@0: if (!aEvent.dataTransfer.mozTypesAt(0)) {
michael@0: return;
michael@0: }
michael@0:
michael@0: let draggedItemId =
michael@0: aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
michael@0:
michael@0: let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
michael@0: draggedWrapper.hidden = false;
michael@0: draggedWrapper.removeAttribute("mousedown");
michael@0: if (this._dragOverItem) {
michael@0: this._cancelDragActive(this._dragOverItem);
michael@0: this._dragOverItem = null;
michael@0: }
michael@0: this._updateToolbarCustomizationOutline(this.window);
michael@0: this._showPanelCustomizationPlaceholders();
michael@0: DragPositionManager.stop();
michael@0: },
michael@0:
michael@0: _isUnwantedDragDrop: function(aEvent) {
michael@0: // The simulated events generated by synthesizeDragStart/synthesizeDrop in
michael@0: // mochitests are used only for testing whether the right data is being put
michael@0: // into the dataTransfer. Neither cause a real drop to occur, so they don't
michael@0: // set the source node. There isn't a means of testing real drag and drops,
michael@0: // so this pref skips the check but it should only be set by test code.
michael@0: if (this._skipSourceNodeCheck) {
michael@0: return false;
michael@0: }
michael@0:
michael@0: /* Discard drag events that originated from a separate window to
michael@0: prevent content->chrome privilege escalations. */
michael@0: let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
michael@0: // mozSourceNode is null in the dragStart event handler or if
michael@0: // the drag event originated in an external application.
michael@0: return !mozSourceNode ||
michael@0: mozSourceNode.ownerDocument.defaultView != this.window;
michael@0: },
michael@0:
michael@0: _setDragActive: function(aItem, aValue, aDraggedItemId, aInToolbar) {
michael@0: if (!aItem) {
michael@0: return;
michael@0: }
michael@0:
michael@0: if (aItem.getAttribute("dragover") != aValue) {
michael@0: aItem.setAttribute("dragover", aValue);
michael@0:
michael@0: let window = aItem.ownerDocument.defaultView;
michael@0: let draggedItem = window.document.getElementById(aDraggedItemId);
michael@0: if (!aInToolbar) {
michael@0: this._setGridDragActive(aItem, draggedItem, aValue);
michael@0: } else {
michael@0: let targetArea = this._getCustomizableParent(aItem);
michael@0: this._updateToolbarCustomizationOutline(window, targetArea);
michael@0: let makeSpaceImmediately = false;
michael@0: if (!gDraggingInToolbars.has(targetArea.id)) {
michael@0: gDraggingInToolbars.add(targetArea.id);
michael@0: let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItemId);
michael@0: let originArea = this._getCustomizableParent(draggedWrapper);
michael@0: makeSpaceImmediately = originArea == targetArea;
michael@0: }
michael@0: // Calculate width of the item when it'd be dropped in this position
michael@0: let width = this._getDragItemSize(aItem, draggedItem).width;
michael@0: let direction = window.getComputedStyle(aItem).direction;
michael@0: let prop, otherProp;
michael@0: // If we're inserting before in ltr, or after in rtl:
michael@0: if ((aValue == "before") == (direction == "ltr")) {
michael@0: prop = "borderLeftWidth";
michael@0: otherProp = "border-right-width";
michael@0: } else {
michael@0: // otherwise:
michael@0: prop = "borderRightWidth";
michael@0: otherProp = "border-left-width";
michael@0: }
michael@0: if (makeSpaceImmediately) {
michael@0: aItem.setAttribute("notransition", "true");
michael@0: }
michael@0: aItem.style[prop] = width + 'px';
michael@0: aItem.style.removeProperty(otherProp);
michael@0: if (makeSpaceImmediately) {
michael@0: // Force a layout flush:
michael@0: aItem.getBoundingClientRect();
michael@0: aItem.removeAttribute("notransition");
michael@0: }
michael@0: }
michael@0: }
michael@0: },
michael@0: _cancelDragActive: function(aItem, aNextItem, aNoTransition) {
michael@0: this._updateToolbarCustomizationOutline(aItem.ownerDocument.defaultView);
michael@0: let currentArea = this._getCustomizableParent(aItem);
michael@0: if (!currentArea) {
michael@0: return;
michael@0: }
michael@0: let isToolbar = CustomizableUI.getAreaType(currentArea.id) == "toolbar";
michael@0: if (isToolbar) {
michael@0: if (aNoTransition) {
michael@0: aItem.setAttribute("notransition", "true");
michael@0: }
michael@0: aItem.removeAttribute("dragover");
michael@0: // Remove both property values in the case that the end padding
michael@0: // had been set.
michael@0: aItem.style.removeProperty("border-left-width");
michael@0: aItem.style.removeProperty("border-right-width");
michael@0: if (aNoTransition) {
michael@0: // Force a layout flush:
michael@0: aItem.getBoundingClientRect();
michael@0: aItem.removeAttribute("notransition");
michael@0: }
michael@0: } else {
michael@0: aItem.removeAttribute("dragover");
michael@0: if (aNextItem) {
michael@0: let nextArea = this._getCustomizableParent(aNextItem);
michael@0: if (nextArea == currentArea) {
michael@0: // No need to do anything if we're still dragging in this area:
michael@0: return;
michael@0: }
michael@0: }
michael@0: // Otherwise, clear everything out:
michael@0: let positionManager = DragPositionManager.getManagerForArea(currentArea);
michael@0: positionManager.clearPlaceholders(currentArea, aNoTransition);
michael@0: }
michael@0: },
michael@0:
michael@0: _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) {
michael@0: let targetArea = this._getCustomizableParent(aDragOverNode);
michael@0: let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItem.id);
michael@0: let originArea = this._getCustomizableParent(draggedWrapper);
michael@0: let positionManager = DragPositionManager.getManagerForArea(targetArea);
michael@0: let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
michael@0: let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
michael@0: positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize,
michael@0: originArea == targetArea);
michael@0: },
michael@0:
michael@0: _getDragItemSize: function(aDragOverNode, aDraggedItem) {
michael@0: // Cache it good, cache it real good.
michael@0: if (!this._dragSizeMap)
michael@0: this._dragSizeMap = new WeakMap();
michael@0: if (!this._dragSizeMap.has(aDraggedItem))
michael@0: this._dragSizeMap.set(aDraggedItem, new WeakMap());
michael@0: let itemMap = this._dragSizeMap.get(aDraggedItem);
michael@0: let targetArea = this._getCustomizableParent(aDragOverNode);
michael@0: let currentArea = this._getCustomizableParent(aDraggedItem);
michael@0: // Return the size for this target from cache, if it exists.
michael@0: let size = itemMap.get(targetArea);
michael@0: if (size)
michael@0: return size;
michael@0:
michael@0: // Calculate size of the item when it'd be dropped in this position.
michael@0: let currentParent = aDraggedItem.parentNode;
michael@0: let currentSibling = aDraggedItem.nextSibling;
michael@0: const kAreaType = "cui-areatype";
michael@0: let areaType, currentType;
michael@0:
michael@0: if (targetArea != currentArea) {
michael@0: // Move the widget temporarily next to the placeholder.
michael@0: aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
michael@0: // Update the node's areaType.
michael@0: areaType = CustomizableUI.getAreaType(targetArea.id);
michael@0: currentType = aDraggedItem.hasAttribute(kAreaType) &&
michael@0: aDraggedItem.getAttribute(kAreaType);
michael@0: if (areaType)
michael@0: aDraggedItem.setAttribute(kAreaType, areaType);
michael@0: this.wrapToolbarItem(aDraggedItem, areaType || "palette");
michael@0: CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
michael@0: } else {
michael@0: aDraggedItem.parentNode.hidden = false;
michael@0: }
michael@0:
michael@0: // Fetch the new size.
michael@0: let rect = aDraggedItem.parentNode.getBoundingClientRect();
michael@0: size = {width: rect.width, height: rect.height};
michael@0: // Cache the found value of size for this target.
michael@0: itemMap.set(targetArea, size);
michael@0:
michael@0: if (targetArea != currentArea) {
michael@0: this.unwrapToolbarItem(aDraggedItem.parentNode);
michael@0: // Put the item back into its previous position.
michael@0: currentParent.insertBefore(aDraggedItem, currentSibling);
michael@0: // restore the areaType
michael@0: if (areaType) {
michael@0: if (currentType === false)
michael@0: aDraggedItem.removeAttribute(kAreaType);
michael@0: else
michael@0: aDraggedItem.setAttribute(kAreaType, currentType);
michael@0: }
michael@0: this.createOrUpdateWrapper(aDraggedItem, null, true);
michael@0: CustomizableUI.onWidgetDrag(aDraggedItem.id);
michael@0: } else {
michael@0: aDraggedItem.parentNode.hidden = true;
michael@0: }
michael@0: return size;
michael@0: },
michael@0:
michael@0: _getCustomizableParent: function(aElement) {
michael@0: let areas = CustomizableUI.areas;
michael@0: areas.push(kPaletteId);
michael@0: while (aElement) {
michael@0: if (areas.indexOf(aElement.id) != -1) {
michael@0: return aElement;
michael@0: }
michael@0: aElement = aElement.parentNode;
michael@0: }
michael@0: return null;
michael@0: },
michael@0:
michael@0: _getDragOverNode: function(aEvent, aAreaElement, aInToolbar, aDraggedItemId) {
michael@0: let expectedParent = aAreaElement.customizationTarget || aAreaElement;
michael@0: // Our tests are stupid. Cope:
michael@0: if (!aEvent.clientX && !aEvent.clientY) {
michael@0: return aEvent.target;
michael@0: }
michael@0: // Offset the drag event's position with the offset to the center of
michael@0: // the thing we're dragging
michael@0: let dragX = aEvent.clientX - this._dragOffset.x;
michael@0: let dragY = aEvent.clientY - this._dragOffset.y;
michael@0:
michael@0: // Ensure this is within the container
michael@0: let boundsContainer = expectedParent;
michael@0: // NB: because the panel UI itself is inside a scrolling container, we need
michael@0: // to use the parent bounds (otherwise, if the panel UI is scrolled down,
michael@0: // the numbers we get are in window coordinates which leads to various kinds
michael@0: // of weirdness)
michael@0: if (boundsContainer == this.panelUIContents) {
michael@0: boundsContainer = boundsContainer.parentNode;
michael@0: }
michael@0: let bounds = boundsContainer.getBoundingClientRect();
michael@0: dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
michael@0: dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
michael@0:
michael@0: let targetNode;
michael@0: if (aInToolbar) {
michael@0: targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
michael@0: while (targetNode && targetNode.parentNode != expectedParent) {
michael@0: targetNode = targetNode.parentNode;
michael@0: }
michael@0: } else {
michael@0: let positionManager = DragPositionManager.getManagerForArea(aAreaElement);
michael@0: // Make it relative to the container:
michael@0: dragX -= bounds.left;
michael@0: // NB: but if we're in the panel UI, we need to use the actual panel
michael@0: // contents instead of the scrolling container to determine our origin
michael@0: // offset against:
michael@0: if (expectedParent == this.panelUIContents) {
michael@0: dragY -= this.panelUIContents.getBoundingClientRect().top;
michael@0: } else {
michael@0: dragY -= bounds.top;
michael@0: }
michael@0: // Find the closest node:
michael@0: targetNode = positionManager.find(aAreaElement, dragX, dragY, aDraggedItemId);
michael@0: }
michael@0: return targetNode || aEvent.target;
michael@0: },
michael@0:
michael@0: _onMouseDown: function(aEvent) {
michael@0: LOG("_onMouseDown");
michael@0: if (aEvent.button != 0) {
michael@0: return;
michael@0: }
michael@0: let doc = aEvent.target.ownerDocument;
michael@0: doc.documentElement.setAttribute("customizing-movingItem", true);
michael@0: let item = this._getWrapper(aEvent.target);
michael@0: if (item && !item.classList.contains(kPlaceholderClass) &&
michael@0: item.getAttribute("removable") == "true") {
michael@0: item.setAttribute("mousedown", "true");
michael@0: }
michael@0: },
michael@0:
michael@0: _onMouseUp: function(aEvent) {
michael@0: LOG("_onMouseUp");
michael@0: if (aEvent.button != 0) {
michael@0: return;
michael@0: }
michael@0: let doc = aEvent.target.ownerDocument;
michael@0: doc.documentElement.removeAttribute("customizing-movingItem");
michael@0: let item = this._getWrapper(aEvent.target);
michael@0: if (item) {
michael@0: item.removeAttribute("mousedown");
michael@0: }
michael@0: },
michael@0:
michael@0: _getWrapper: function(aElement) {
michael@0: while (aElement && aElement.localName != "toolbarpaletteitem") {
michael@0: if (aElement.localName == "toolbar")
michael@0: return null;
michael@0: aElement = aElement.parentNode;
michael@0: }
michael@0: return aElement;
michael@0: },
michael@0:
michael@0: _showPanelCustomizationPlaceholders: function() {
michael@0: let doc = this.document;
michael@0: let contents = this.panelUIContents;
michael@0: let narrowItemsAfterWideItem = 0;
michael@0: let node = contents.lastChild;
michael@0: while (node && !node.classList.contains(CustomizableUI.WIDE_PANEL_CLASS) &&
michael@0: (!node.firstChild || !node.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS))) {
michael@0: if (!node.hidden && !node.classList.contains(kPlaceholderClass)) {
michael@0: narrowItemsAfterWideItem++;
michael@0: }
michael@0: node = node.previousSibling;
michael@0: }
michael@0:
michael@0: let orphanedItems = narrowItemsAfterWideItem % CustomizableUI.PANEL_COLUMN_COUNT;
michael@0: let placeholders = CustomizableUI.PANEL_COLUMN_COUNT - orphanedItems;
michael@0:
michael@0: let currentPlaceholderCount = contents.querySelectorAll("." + kPlaceholderClass).length;
michael@0: if (placeholders > currentPlaceholderCount) {
michael@0: while (placeholders-- > currentPlaceholderCount) {
michael@0: let placeholder = doc.createElement("toolbarpaletteitem");
michael@0: placeholder.classList.add(kPlaceholderClass);
michael@0: //XXXjaws The toolbarbutton child here is only necessary to get
michael@0: // the styling right here.
michael@0: let placeholderChild = doc.createElement("toolbarbutton");
michael@0: placeholderChild.classList.add(kPlaceholderClass + "-child");
michael@0: placeholder.appendChild(placeholderChild);
michael@0: contents.appendChild(placeholder);
michael@0: }
michael@0: } else if (placeholders < currentPlaceholderCount) {
michael@0: while (placeholders++ < currentPlaceholderCount) {
michael@0: contents.querySelectorAll("." + kPlaceholderClass)[0].remove();
michael@0: }
michael@0: }
michael@0: },
michael@0:
michael@0: _removePanelCustomizationPlaceholders: function() {
michael@0: let contents = this.panelUIContents;
michael@0: let oldPlaceholders = contents.getElementsByClassName(kPlaceholderClass);
michael@0: while (oldPlaceholders.length) {
michael@0: contents.removeChild(oldPlaceholders[0]);
michael@0: }
michael@0: },
michael@0:
michael@0: /**
michael@0: * Update toolbar customization targets during drag events to add or remove
michael@0: * outlines to indicate that an area is customizable.
michael@0: *
michael@0: * @param aWindow The XUL window in which outlines should be updated.
michael@0: * @param {Element} [aToolbarArea=null] The element of the customizable toolbar area to add the
michael@0: * outline to. If aToolbarArea is falsy, the outline will be
michael@0: * removed from all toolbar areas.
michael@0: */
michael@0: _updateToolbarCustomizationOutline: function(aWindow, aToolbarArea = null) {
michael@0: // Remove the attribute from existing customization targets
michael@0: for (let area of CustomizableUI.areas) {
michael@0: if (CustomizableUI.getAreaType(area) != CustomizableUI.TYPE_TOOLBAR) {
michael@0: continue;
michael@0: }
michael@0: let target = CustomizableUI.getCustomizeTargetForArea(area, aWindow);
michael@0: target.removeAttribute("customizing-dragovertarget");
michael@0: }
michael@0:
michael@0: // Now set the attribute on the desired target
michael@0: if (aToolbarArea) {
michael@0: if (CustomizableUI.getAreaType(aToolbarArea.id) != CustomizableUI.TYPE_TOOLBAR)
michael@0: return;
michael@0: let target = CustomizableUI.getCustomizeTargetForArea(aToolbarArea.id, aWindow);
michael@0: target.setAttribute("customizing-dragovertarget", true);
michael@0: }
michael@0: },
michael@0:
michael@0: _findVisiblePreviousSiblingNode: function(aReferenceNode) {
michael@0: while (aReferenceNode &&
michael@0: aReferenceNode.localName == "toolbarpaletteitem" &&
michael@0: aReferenceNode.firstChild.hidden) {
michael@0: aReferenceNode = aReferenceNode.previousSibling;
michael@0: }
michael@0: return aReferenceNode;
michael@0: },
michael@0: };
michael@0:
michael@0: function __dumpDragData(aEvent, caller) {
michael@0: if (!gDebug) {
michael@0: return;
michael@0: }
michael@0: let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
michael@0: str += " type: " + aEvent["type"] + "\n";
michael@0: for (let el of ["target", "currentTarget", "relatedTarget"]) {
michael@0: if (aEvent[el]) {
michael@0: str += " " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n";
michael@0: }
michael@0: }
michael@0: for (let prop in aEvent.dataTransfer) {
michael@0: if (typeof aEvent.dataTransfer[prop] != "function") {
michael@0: str += " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
michael@0: }
michael@0: }
michael@0: str += "}";
michael@0: LOG(str);
michael@0: }
michael@0:
michael@0: function dispatchFunction(aFunc) {
michael@0: Services.tm.currentThread.dispatch(aFunc, Ci.nsIThread.DISPATCH_NORMAL);
michael@0: }