browser/components/customizableui/src/CustomizeMode.jsm

Wed, 31 Dec 2014 06:55:46 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:46 +0100
changeset 1
ca08bd8f51b2
permissions
-rw-r--r--

Added tag TORBROWSER_REPLICA for changeset 6474c204b198

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["CustomizeMode"];
     9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    11 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
    12 const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation";
    13 const kPaletteId = "customization-palette";
    14 const kAboutURI = "about:customizing";
    15 const kDragDataTypePrefix = "text/toolbarwrapper-id/";
    16 const kPlaceholderClass = "panel-customization-placeholder";
    17 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
    18 const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
    19 const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
    20 const kMaxTransitionDurationMs = 2000;
    22 const kPanelItemContextMenu = "customizationPanelItemContextMenu";
    23 const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
    25 Cu.import("resource://gre/modules/Services.jsm");
    26 Cu.import("resource:///modules/CustomizableUI.jsm");
    27 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    28 Cu.import("resource://gre/modules/Task.jsm");
    29 Cu.import("resource://gre/modules/Promise.jsm");
    31 XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager",
    32                                   "resource:///modules/DragPositionManager.jsm");
    33 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
    34                                   "resource:///modules/BrowserUITelemetry.jsm");
    36 let gModuleName = "[CustomizeMode]";
    37 #include logging.js
    39 let gDisableAnimation = null;
    41 let gDraggingInToolbars;
    43 function CustomizeMode(aWindow) {
    44   if (gDisableAnimation === null) {
    45     gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL &&
    46                         Services.prefs.getBoolPref(kPrefCustomizationAnimation);
    47   }
    48   this.window = aWindow;
    49   this.document = aWindow.document;
    50   this.browser = aWindow.gBrowser;
    52   // There are two palettes - there's the palette that can be overlayed with
    53   // toolbar items in browser.xul. This is invisible, and never seen by the
    54   // user. Then there's the visible palette, which gets populated and displayed
    55   // to the user when in customizing mode.
    56   this.visiblePalette = this.document.getElementById(kPaletteId);
    57   this.paletteEmptyNotice = this.document.getElementById("customization-empty");
    58   this.paletteSpacer = this.document.getElementById("customization-spacer");
    59   this.tipPanel = this.document.getElementById("customization-tipPanel");
    60 #ifdef CAN_DRAW_IN_TITLEBAR
    61   this._updateTitlebarButton();
    62   Services.prefs.addObserver(kDrawInTitlebarPref, this, false);
    63   this.window.addEventListener("unload", this);
    64 #endif
    65 };
    67 CustomizeMode.prototype = {
    68   _changed: false,
    69   _transitioning: false,
    70   window: null,
    71   document: null,
    72   // areas is used to cache the customizable areas when in customization mode.
    73   areas: null,
    74   // When in customizing mode, we swap out the reference to the invisible
    75   // palette in gNavToolbox.palette for our visiblePalette. This way, for the
    76   // customizing browser window, when widgets are removed from customizable
    77   // areas and added to the palette, they're added to the visible palette.
    78   // _stowedPalette is a reference to the old invisible palette so we can
    79   // restore gNavToolbox.palette to its original state after exiting
    80   // customization mode.
    81   _stowedPalette: null,
    82   _dragOverItem: null,
    83   _customizing: false,
    84   _skipSourceNodeCheck: null,
    85   _mainViewContext: null,
    87   get panelUIContents() {
    88     return this.document.getElementById("PanelUI-contents");
    89   },
    91   get _handler() {
    92     return this.window.CustomizationHandler;
    93   },
    95   uninit: function() {
    96 #ifdef CAN_DRAW_IN_TITLEBAR
    97     Services.prefs.removeObserver(kDrawInTitlebarPref, this);
    98 #endif
    99   },
   101   toggle: function() {
   102     if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
   103       this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
   104       return;
   105     }
   106     if (this._customizing) {
   107       this.exit();
   108     } else {
   109       this.enter();
   110     }
   111   },
   113   enter: function() {
   114     this._wantToBeInCustomizeMode = true;
   116     if (this._customizing || this._handler.isEnteringCustomizeMode) {
   117       return;
   118     }
   120     // Exiting; want to re-enter once we've done that.
   121     if (this._handler.isExitingCustomizeMode) {
   122       LOG("Attempted to enter while we're in the middle of exiting. " +
   123           "We'll exit after we've entered");
   124       return;
   125     }
   128     // We don't need to switch to kAboutURI, or open a new tab at
   129     // kAboutURI if we're already on it.
   130     if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) {
   131       this.window.switchToTabHavingURI(kAboutURI, true, {
   132         skipTabAnimation: true,
   133       });
   134       return;
   135     }
   137     let window = this.window;
   138     let document = this.document;
   140     this._handler.isEnteringCustomizeMode = true;
   142     // Always disable the reset button at the start of customize mode, it'll be re-enabled
   143     // if necessary when we finish entering:
   144     let resetButton = this.document.getElementById("customization-reset-button");
   145     resetButton.setAttribute("disabled", "true");
   147     Task.spawn(function() {
   148       // We shouldn't start customize mode until after browser-delayed-startup has finished:
   149       if (!this.window.gBrowserInit.delayedStartupFinished) {
   150         let delayedStartupDeferred = Promise.defer();
   151         let delayedStartupObserver = function(aSubject) {
   152           if (aSubject == this.window) {
   153             Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
   154             delayedStartupDeferred.resolve();
   155           }
   156         }.bind(this);
   157         Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
   158         yield delayedStartupDeferred.promise;
   159       }
   161       let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
   162       let togglableToolbars = window.getTogglableToolbars();
   163       let bookmarksToolbar = document.getElementById("PersonalToolbar");
   164       if (togglableToolbars.length == 0) {
   165         toolbarVisibilityBtn.setAttribute("hidden", "true");
   166       } else {
   167         toolbarVisibilityBtn.removeAttribute("hidden");
   168       }
   170       // Disable lightweight themes while in customization mode since
   171       // they don't have large enough images to pad the full browser window.
   172       if (this.document.documentElement._lightweightTheme)
   173         this.document.documentElement._lightweightTheme.disable();
   175       CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
   176       CustomizableUI.notifyStartCustomizing(this.window);
   178       // Add a keypress listener to the document so that we can quickly exit
   179       // customization mode when pressing ESC.
   180       document.addEventListener("keypress", this);
   182       // Same goes for the menu button - if we're customizing, a click on the
   183       // menu button means a quick exit from customization mode.
   184       window.PanelUI.hide();
   185       window.PanelUI.menuButton.addEventListener("command", this);
   186       window.PanelUI.menuButton.open = true;
   187       window.PanelUI.beginBatchUpdate();
   189       // The menu panel is lazy, and registers itself when the popup shows. We
   190       // need to force the menu panel to register itself, or else customization
   191       // is really not going to work. We pass "true" to ensureReady to
   192       // indicate that we're handling calling startBatchUpdate and
   193       // endBatchUpdate.
   194       if (!window.PanelUI.isReady()) {
   195         yield window.PanelUI.ensureReady(true);
   196       }
   198       // Hide the palette before starting the transition for increased perf.
   199       this.visiblePalette.hidden = true;
   200       this.visiblePalette.removeAttribute("showing");
   202       // Disable the button-text fade-out mask
   203       // during the transition for increased perf.
   204       let panelContents = window.PanelUI.contents;
   205       panelContents.setAttribute("customize-transitioning", "true");
   207       // Move the mainView in the panel to the holder so that we can see it
   208       // while customizing.
   209       let mainView = window.PanelUI.mainView;
   210       let panelHolder = document.getElementById("customization-panelHolder");
   211       panelHolder.appendChild(mainView);
   213       let customizeButton = document.getElementById("PanelUI-customize");
   214       customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label"));
   215       customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel"));
   216       customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext"));
   217       customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext"));
   219       this._transitioning = true;
   221       let customizer = document.getElementById("customization-container");
   222       customizer.parentNode.selectedPanel = customizer;
   223       customizer.hidden = false;
   225       yield this._doTransition(true);
   227       // Let everybody in this window know that we're about to customize.
   228       CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
   230       this._mainViewContext = mainView.getAttribute("context");
   231       if (this._mainViewContext) {
   232         mainView.removeAttribute("context");
   233       }
   235       this._showPanelCustomizationPlaceholders();
   237       yield this._wrapToolbarItems();
   238       this.populatePalette();
   240       this._addDragHandlers(this.visiblePalette);
   242       window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
   244       document.getElementById("PanelUI-help").setAttribute("disabled", true);
   245       document.getElementById("PanelUI-quit").setAttribute("disabled", true);
   247       this._updateResetButton();
   248       this._updateUndoResetButton();
   250       this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
   251                                   Services.prefs.getBoolPref(kSkipSourceNodePref);
   253       let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
   254       for (let toolbar of customizableToolbars)
   255         toolbar.setAttribute("customizing", true);
   257       CustomizableUI.addListener(this);
   258       window.PanelUI.endBatchUpdate();
   259       this._customizing = true;
   260       this._transitioning = false;
   262       // Show the palette now that the transition has finished.
   263       this.visiblePalette.hidden = false;
   264       window.setTimeout(() => {
   265         // Force layout reflow to ensure the animation runs,
   266         // and make it async so it doesn't affect the timing.
   267         this.visiblePalette.clientTop;
   268         this.visiblePalette.setAttribute("showing", "true");
   269       }, 0);
   270       this.paletteSpacer.hidden = true;
   271       this._updateEmptyPaletteNotice();
   273       this.maybeShowTip(panelHolder);
   275       this._handler.isEnteringCustomizeMode = false;
   276       panelContents.removeAttribute("customize-transitioning");
   278       CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
   279       this._enableOutlinesTimeout = window.setTimeout(() => {
   280         this.document.getElementById("nav-bar").setAttribute("showoutline", "true");
   281         this.panelUIContents.setAttribute("showoutline", "true");
   282         delete this._enableOutlinesTimeout;
   283       }, 0);
   285       // It's possible that we didn't enter customize mode via the menu panel,
   286       // meaning we didn't kick off about:customizing preloading. If that's
   287       // the case, let's kick it off for the next time we load this mode.
   288       window.gCustomizationTabPreloader.ensurePreloading();
   289       if (!this._wantToBeInCustomizeMode) {
   290         this.exit();
   291       }
   292     }.bind(this)).then(null, function(e) {
   293       ERROR(e);
   294       // We should ensure this has been called, and calling it again doesn't hurt:
   295       window.PanelUI.endBatchUpdate();
   296       this._handler.isEnteringCustomizeMode = false;
   297     }.bind(this));
   298   },
   300   exit: function() {
   301     this._wantToBeInCustomizeMode = false;
   303     if (!this._customizing || this._handler.isExitingCustomizeMode) {
   304       return;
   305     }
   307     // Entering; want to exit once we've done that.
   308     if (this._handler.isEnteringCustomizeMode) {
   309       LOG("Attempted to exit while we're in the middle of entering. " +
   310           "We'll exit after we've entered");
   311       return;
   312     }
   314     if (this.resetting) {
   315       LOG("Attempted to exit while we're resetting. " +
   316           "We'll exit after resetting has finished.");
   317       return;
   318     }
   320     this.hideTip();
   322     this._handler.isExitingCustomizeMode = true;
   324     if (this._enableOutlinesTimeout) {
   325       this.window.clearTimeout(this._enableOutlinesTimeout);
   326     } else {
   327       this.document.getElementById("nav-bar").removeAttribute("showoutline");
   328       this.panelUIContents.removeAttribute("showoutline");
   329     }
   331     this._removeExtraToolbarsIfEmpty();
   333     CustomizableUI.removeListener(this);
   335     this.document.removeEventListener("keypress", this);
   336     this.window.PanelUI.menuButton.removeEventListener("command", this);
   337     this.window.PanelUI.menuButton.open = false;
   339     this.window.PanelUI.beginBatchUpdate();
   341     this._removePanelCustomizationPlaceholders();
   343     let window = this.window;
   344     let document = this.document;
   345     let documentElement = document.documentElement;
   347     // Hide the palette before starting the transition for increased perf.
   348     this.paletteSpacer.hidden = false;
   349     this.visiblePalette.hidden = true;
   350     this.visiblePalette.removeAttribute("showing");
   351     this.paletteEmptyNotice.hidden = true;
   353     // Disable the button-text fade-out mask
   354     // during the transition for increased perf.
   355     let panelContents = window.PanelUI.contents;
   356     panelContents.setAttribute("customize-transitioning", "true");
   358     // Disable the reset and undo reset buttons while transitioning:
   359     let resetButton = this.document.getElementById("customization-reset-button");
   360     let undoResetButton = this.document.getElementById("customization-undo-reset-button");
   361     undoResetButton.hidden = resetButton.disabled = true;
   363     this._transitioning = true;
   365     Task.spawn(function() {
   366       yield this.depopulatePalette();
   368       yield this._doTransition(false);
   370       let browser = document.getElementById("browser");
   371       if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) {
   372         let custBrowser = this.browser.selectedBrowser;
   373         if (custBrowser.canGoBack) {
   374           // If there's history to this tab, just go back.
   375           // Note that this throws an exception if the previous document has a
   376           // problematic URL (e.g. about:idontexist)
   377           try {
   378             custBrowser.goBack();
   379           } catch (ex) {
   380             ERROR(ex);
   381           }
   382         } else {
   383           // If we can't go back, we're removing the about:customization tab.
   384           // We only do this if we're the top window for this window (so not
   385           // a dialog window, for example).
   386           if (window.getTopWin(true) == window) {
   387             let customizationTab = this.browser.selectedTab;
   388             if (this.browser.browsers.length == 1) {
   389               window.BrowserOpenTab();
   390             }
   391             this.browser.removeTab(customizationTab);
   392           }
   393         }
   394       }
   395       browser.parentNode.selectedPanel = browser;
   396       let customizer = document.getElementById("customization-container");
   397       customizer.hidden = true;
   399       window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
   401       DragPositionManager.stop();
   402       this._removeDragHandlers(this.visiblePalette);
   404       yield this._unwrapToolbarItems();
   406       if (this._changed) {
   407         // XXXmconley: At first, it seems strange to also persist the old way with
   408         //             currentset - but this might actually be useful for switching
   409         //             to old builds. We might want to keep this around for a little
   410         //             bit.
   411         this.persistCurrentSets();
   412       }
   414       // And drop all area references.
   415       this.areas = [];
   417       // Let everybody in this window know that we're starting to
   418       // exit customization mode.
   419       CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
   421       window.PanelUI.setMainView(window.PanelUI.mainView);
   422       window.PanelUI.menuButton.disabled = false;
   424       let customizeButton = document.getElementById("PanelUI-customize");
   425       customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label"));
   426       customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel"));
   427       customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext"));
   428       customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext"));
   430       // We have to use setAttribute/removeAttribute here instead of the
   431       // property because the XBL property will be set later, and right
   432       // now we'd be setting an expando, which breaks the XBL property.
   433       document.getElementById("PanelUI-help").removeAttribute("disabled");
   434       document.getElementById("PanelUI-quit").removeAttribute("disabled");
   436       panelContents.removeAttribute("customize-transitioning");
   438       // We need to set this._customizing to false before removing the tab
   439       // or the TabSelect event handler will think that we are exiting
   440       // customization mode for a second time.
   441       this._customizing = false;
   443       let mainView = window.PanelUI.mainView;
   444       if (this._mainViewContext) {
   445         mainView.setAttribute("context", this._mainViewContext);
   446       }
   448       if (this.document.documentElement._lightweightTheme)
   449         this.document.documentElement._lightweightTheme.enable();
   451       let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])");
   452       for (let toolbar of customizableToolbars)
   453         toolbar.removeAttribute("customizing");
   455       this.window.PanelUI.endBatchUpdate();
   456       this._changed = false;
   457       this._transitioning = false;
   458       this._handler.isExitingCustomizeMode = false;
   459       CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
   460       CustomizableUI.notifyEndCustomizing(window);
   462       if (this._wantToBeInCustomizeMode) {
   463         this.enter();
   464       }
   465     }.bind(this)).then(null, function(e) {
   466       ERROR(e);
   467       // We should ensure this has been called, and calling it again doesn't hurt:
   468       window.PanelUI.endBatchUpdate();
   469       this._handler.isExitingCustomizeMode = false;
   470     }.bind(this));
   471   },
   473   /**
   474    * The customize mode transition has 3 phases when entering:
   475    * 1) Pre-customization mode
   476    *    This is the starting phase of the browser.
   477    * 2) customize-entering
   478    *    This phase is a transition, optimized for smoothness.
   479    * 3) customize-entered
   480    *    After the transition completes, this phase draws all of the
   481    *    expensive detail that isn't necessary during the second phase.
   482    *
   483    * Exiting customization mode has a similar set of phases, but in reverse
   484    * order - customize-entered, customize-exiting, pre-customization mode.
   485    *
   486    * When in the customize-entering, customize-entered, or customize-exiting
   487    * phases, there is a "customizing" attribute set on the main-window to simplify
   488    * excluding certain styles while in any phase of customize mode.
   489    */
   490   _doTransition: function(aEntering) {
   491     let deferred = Promise.defer();
   492     let deck = this.document.getElementById("content-deck");
   494     let customizeTransitionEnd = function(aEvent) {
   495       if (aEvent != "timedout" &&
   496           (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
   497         return;
   498       }
   499       this.window.clearTimeout(catchAllTimeout);
   500       // Bug 962677: We let the event loop breathe for before we do the final
   501       // stage of the transition to improve perceived performance.
   502       this.window.setTimeout(function () {
   503         deck.removeEventListener("transitionend", customizeTransitionEnd);
   505         if (!aEntering) {
   506           this.document.documentElement.removeAttribute("customize-exiting");
   507           this.document.documentElement.removeAttribute("customizing");
   508         } else {
   509           this.document.documentElement.setAttribute("customize-entered", true);
   510           this.document.documentElement.removeAttribute("customize-entering");
   511         }
   512         CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
   514         deferred.resolve();
   515       }.bind(this), 0);
   516     }.bind(this);
   517     deck.addEventListener("transitionend", customizeTransitionEnd);
   519     if (gDisableAnimation) {
   520       this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true);
   521     }
   522     if (aEntering) {
   523       this.document.documentElement.setAttribute("customizing", true);
   524       this.document.documentElement.setAttribute("customize-entering", true);
   525     } else {
   526       this.document.documentElement.setAttribute("customize-exiting", true);
   527       this.document.documentElement.removeAttribute("customize-entered");
   528     }
   530     let catchAll = () => customizeTransitionEnd("timedout");
   531     let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
   532     return deferred.promise;
   533   },
   535   maybeShowTip: function(aAnchor) {
   536     let shown = false;
   537     const kShownPref = "browser.customizemode.tip0.shown";
   538     try {
   539       shown = Services.prefs.getBoolPref(kShownPref);
   540     } catch (ex) {}
   541     if (shown)
   542       return;
   544     let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder");
   545     let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage");
   546     if (!messageNode.childElementCount) {
   547       // Put the tip contents in the popup.
   548       let bundle = this.document.getElementById("bundle_browser");
   549       const kLabelClass = "customization-tipPanel-link";
   550       messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [
   551         "<label class=\"customization-tipPanel-em\" value=\"" +
   552           bundle.getString("customizeTips.tip0.hint") + "\"/>",
   553         this.document.getElementById("bundle_brand").getString("brandShortName"),
   554         "<label class=\"" + kLabelClass + " text-link\" value=\"" +
   555         bundle.getString("customizeTips.tip0.learnMore") + "\"/>"
   556       ]);
   558       messageNode.querySelector("." + kLabelClass).addEventListener("click", () => {
   559         let url = Services.urlFormatter.formatURLPref("browser.customizemode.tip0.learnMoreUrl");
   560         let browser = this.browser;
   561         browser.selectedTab = browser.addTab(url);
   562         this.hideTip();
   563       });
   564     }
   566     this.tipPanel.hidden = false;
   567     this.tipPanel.openPopup(anchorNode);
   568     Services.prefs.setBoolPref(kShownPref, true);
   569   },
   571   hideTip: function() {
   572     this.tipPanel.hidePopup();
   573   },
   575   _getCustomizableChildForNode: function(aNode) {
   576     // NB: adjusted from _getCustomizableParent to keep that method fast
   577     // (it's used during drags), and avoid multiple DOM loops
   578     let areas = CustomizableUI.areas;
   579     // Caching this length is important because otherwise we'll also iterate
   580     // over items we add to the end from within the loop.
   581     let numberOfAreas = areas.length;
   582     for (let i = 0; i < numberOfAreas; i++) {
   583       let area = areas[i];
   584       let areaNode = aNode.ownerDocument.getElementById(area);
   585       let customizationTarget = areaNode && areaNode.customizationTarget;
   586       if (customizationTarget && customizationTarget != areaNode) {
   587         areas.push(customizationTarget.id);
   588       }
   589       let overflowTarget = areaNode.getAttribute("overflowtarget");
   590       if (overflowTarget) {
   591         areas.push(overflowTarget);
   592       }
   593     }
   594     areas.push(kPaletteId);
   596     while (aNode && aNode.parentNode) {
   597       let parent = aNode.parentNode;
   598       if (areas.indexOf(parent.id) != -1) {
   599         return aNode;
   600       }
   601       aNode = parent;
   602     }
   603     return null;
   604   },
   606   addToToolbar: function(aNode) {
   607     aNode = this._getCustomizableChildForNode(aNode);
   608     if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
   609       aNode = aNode.firstChild;
   610     }
   611     CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR);
   612     if (!this._customizing) {
   613       CustomizableUI.dispatchToolboxEvent("customizationchange");
   614     }
   615   },
   617   addToPanel: function(aNode) {
   618     aNode = this._getCustomizableChildForNode(aNode);
   619     if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
   620       aNode = aNode.firstChild;
   621     }
   622     CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_PANEL);
   623     if (!this._customizing) {
   624       CustomizableUI.dispatchToolboxEvent("customizationchange");
   625     }
   626   },
   628   removeFromArea: function(aNode) {
   629     aNode = this._getCustomizableChildForNode(aNode);
   630     if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
   631       aNode = aNode.firstChild;
   632     }
   633     CustomizableUI.removeWidgetFromArea(aNode.id);
   634     if (!this._customizing) {
   635       CustomizableUI.dispatchToolboxEvent("customizationchange");
   636     }
   637   },
   639   populatePalette: function() {
   640     let fragment = this.document.createDocumentFragment();
   641     let toolboxPalette = this.window.gNavToolbox.palette;
   643     try {
   644       let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
   645       for (let widget of unusedWidgets) {
   646         let paletteItem = this.makePaletteItem(widget, "palette");
   647         fragment.appendChild(paletteItem);
   648       }
   650       this.visiblePalette.appendChild(fragment);
   651       this._stowedPalette = this.window.gNavToolbox.palette;
   652       this.window.gNavToolbox.palette = this.visiblePalette;
   653     } catch (ex) {
   654       ERROR(ex);
   655     }
   656   },
   658   //XXXunf Maybe this should use -moz-element instead of wrapping the node?
   659   //       Would ensure no weird interactions/event handling from original node,
   660   //       and makes it possible to put this in a lazy-loaded iframe/real tab
   661   //       while still getting rid of the need for overlays.
   662   makePaletteItem: function(aWidget, aPlace) {
   663     let widgetNode = aWidget.forWindow(this.window).node;
   664     let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
   665     wrapper.appendChild(widgetNode);
   666     return wrapper;
   667   },
   669   depopulatePalette: function() {
   670     return Task.spawn(function() {
   671       this.visiblePalette.hidden = true;
   672       let paletteChild = this.visiblePalette.firstChild;
   673       let nextChild;
   674       while (paletteChild) {
   675         nextChild = paletteChild.nextElementSibling;
   676         let provider = CustomizableUI.getWidget(paletteChild.id).provider;
   677         if (provider == CustomizableUI.PROVIDER_XUL) {
   678           let unwrappedPaletteItem =
   679             yield this.deferredUnwrapToolbarItem(paletteChild);
   680           this._stowedPalette.appendChild(unwrappedPaletteItem);
   681         } else if (provider == CustomizableUI.PROVIDER_API) {
   682           //XXXunf Currently this doesn't destroy the (now unused) node. It would
   683           //       be good to do so, but we need to keep strong refs to it in
   684           //       CustomizableUI (can't iterate of WeakMaps), and there's the
   685           //       question of what behavior wrappers should have if consumers
   686           //       keep hold of them.
   687           //widget.destroyInstance(widgetNode);
   688         } else if (provider == CustomizableUI.PROVIDER_SPECIAL) {
   689           this.visiblePalette.removeChild(paletteChild);
   690         }
   692         paletteChild = nextChild;
   693       }
   694       this.visiblePalette.hidden = false;
   695       this.window.gNavToolbox.palette = this._stowedPalette;
   696     }.bind(this)).then(null, ERROR);
   697   },
   699   isCustomizableItem: function(aNode) {
   700     return aNode.localName == "toolbarbutton" ||
   701            aNode.localName == "toolbaritem" ||
   702            aNode.localName == "toolbarseparator" ||
   703            aNode.localName == "toolbarspring" ||
   704            aNode.localName == "toolbarspacer";
   705   },
   707   isWrappedToolbarItem: function(aNode) {
   708     return aNode.localName == "toolbarpaletteitem";
   709   },
   711   deferredWrapToolbarItem: function(aNode, aPlace) {
   712     let deferred = Promise.defer();
   714     dispatchFunction(function() {
   715       let wrapper = this.wrapToolbarItem(aNode, aPlace);
   716       deferred.resolve(wrapper);
   717     }.bind(this))
   719     return deferred.promise;
   720   },
   722   wrapToolbarItem: function(aNode, aPlace) {
   723     if (!this.isCustomizableItem(aNode)) {
   724       return aNode;
   725     }
   726     let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
   728     // It's possible that this toolbar node is "mid-flight" and doesn't have
   729     // a parent, in which case we skip replacing it. This can happen if a
   730     // toolbar item has been dragged into the palette. In that case, we tell
   731     // CustomizableUI to remove the widget from its area before putting the
   732     // widget in the palette - so the node will have no parent.
   733     if (aNode.parentNode) {
   734       aNode = aNode.parentNode.replaceChild(wrapper, aNode);
   735     }
   736     wrapper.appendChild(aNode);
   737     return wrapper;
   738   },
   740   createOrUpdateWrapper: function(aNode, aPlace, aIsUpdate) {
   741     let wrapper;
   742     if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") {
   743       wrapper = aNode.parentNode;
   744       aPlace = wrapper.getAttribute("place");
   745     } else {
   746       wrapper = this.document.createElement("toolbarpaletteitem");
   747       // "place" is used by toolkit to add the toolbarpaletteitem-palette
   748       // binding to a toolbarpaletteitem, which gives it a label node for when
   749       // it's sitting in the palette.
   750       wrapper.setAttribute("place", aPlace);
   751     }
   754     // Ensure the wrapped item doesn't look like it's in any special state, and
   755     // can't be interactved with when in the customization palette.
   756     if (aNode.hasAttribute("command")) {
   757       wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
   758       aNode.removeAttribute("command");
   759     }
   761     if (aNode.hasAttribute("observes")) {
   762       wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
   763       aNode.removeAttribute("observes");
   764     }
   766     if (aNode.getAttribute("checked") == "true") {
   767       wrapper.setAttribute("itemchecked", "true");
   768       aNode.removeAttribute("checked");
   769     }
   771     if (aNode.hasAttribute("id")) {
   772       wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
   773     }
   775     if (aNode.hasAttribute("label")) {
   776       wrapper.setAttribute("title", aNode.getAttribute("label"));
   777     } else if (aNode.hasAttribute("title")) {
   778       wrapper.setAttribute("title", aNode.getAttribute("title"));
   779     }
   781     if (aNode.hasAttribute("flex")) {
   782       wrapper.setAttribute("flex", aNode.getAttribute("flex"));
   783     }
   785     if (aPlace == "panel") {
   786       if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
   787         wrapper.setAttribute("haswideitem", "true");
   788       } else if (wrapper.hasAttribute("haswideitem")) {
   789         wrapper.removeAttribute("haswideitem");
   790       }
   791     }
   793     let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
   794     wrapper.setAttribute("removable", removable);
   796     let contextMenuAttrName = aNode.getAttribute("context") ? "context" :
   797                                 aNode.getAttribute("contextmenu") ? "contextmenu" : "";
   798     let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
   799     let contextMenuForPlace = aPlace == "panel" ?
   800                                 kPanelItemContextMenu :
   801                                 kPaletteItemContextMenu;
   802     if (aPlace != "toolbar") {
   803       wrapper.setAttribute("context", contextMenuForPlace);
   804     }
   805     // Only keep track of the menu if it is non-default.
   806     if (currentContextMenu &&
   807         currentContextMenu != contextMenuForPlace) {
   808       aNode.setAttribute("wrapped-context", currentContextMenu);
   809       aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName)
   810       aNode.removeAttribute(contextMenuAttrName);
   811     } else if (currentContextMenu == contextMenuForPlace) {
   812       aNode.removeAttribute(contextMenuAttrName);
   813     }
   815     // Only add listeners for newly created wrappers:
   816     if (!aIsUpdate) {
   817       wrapper.addEventListener("mousedown", this);
   818       wrapper.addEventListener("mouseup", this);
   819     }
   821     return wrapper;
   822   },
   824   deferredUnwrapToolbarItem: function(aWrapper) {
   825     let deferred = Promise.defer();
   826     dispatchFunction(function() {
   827       let item = null;
   828       try {
   829         item = this.unwrapToolbarItem(aWrapper);
   830       } catch (ex) {
   831         Cu.reportError(ex);
   832       }
   833       deferred.resolve(item);
   834     }.bind(this));
   835     return deferred.promise;
   836   },
   838   unwrapToolbarItem: function(aWrapper) {
   839     if (aWrapper.nodeName != "toolbarpaletteitem") {
   840       return aWrapper;
   841     }
   842     aWrapper.removeEventListener("mousedown", this);
   843     aWrapper.removeEventListener("mouseup", this);
   845     let place = aWrapper.getAttribute("place");
   847     let toolbarItem = aWrapper.firstChild;
   848     if (!toolbarItem) {
   849       ERROR("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id);
   850       aWrapper.remove();
   851       return null;
   852     }
   854     if (aWrapper.hasAttribute("itemobserves")) {
   855       toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves"));
   856     }
   858     if (aWrapper.hasAttribute("itemchecked")) {
   859       toolbarItem.checked = true;
   860     }
   862     if (aWrapper.hasAttribute("itemcommand")) {
   863       let commandID = aWrapper.getAttribute("itemcommand");
   864       toolbarItem.setAttribute("command", commandID);
   866       //XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
   867       let command = this.document.getElementById(commandID);
   868       if (command && command.hasAttribute("disabled")) {
   869         toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
   870       }
   871     }
   873     let wrappedContext = toolbarItem.getAttribute("wrapped-context");
   874     if (wrappedContext) {
   875       let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
   876       toolbarItem.setAttribute(contextAttrName, wrappedContext);
   877       toolbarItem.removeAttribute("wrapped-contextAttrName");
   878       toolbarItem.removeAttribute("wrapped-context");
   879     } else if (place == "panel") {
   880       toolbarItem.setAttribute("context", kPanelItemContextMenu);
   881     }
   883     if (aWrapper.parentNode) {
   884       aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
   885     }
   886     return toolbarItem;
   887   },
   889   _wrapToolbarItems: function() {
   890     let window = this.window;
   891     // Add drag-and-drop event handlers to all of the customizable areas.
   892     return Task.spawn(function() {
   893       this.areas = [];
   894       for (let area of CustomizableUI.areas) {
   895         let target = CustomizableUI.getCustomizeTargetForArea(area, window);
   896         this._addDragHandlers(target);
   897         for (let child of target.children) {
   898           if (this.isCustomizableItem(child)) {
   899             yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
   900           }
   901         }
   902         this.areas.push(target);
   903       }
   904     }.bind(this)).then(null, ERROR);
   905   },
   907   _addDragHandlers: function(aTarget) {
   908     aTarget.addEventListener("dragstart", this, true);
   909     aTarget.addEventListener("dragover", this, true);
   910     aTarget.addEventListener("dragexit", this, true);
   911     aTarget.addEventListener("drop", this, true);
   912     aTarget.addEventListener("dragend", this, true);
   913   },
   915   _wrapItemsInArea: function(target) {
   916     for (let child of target.children) {
   917       if (this.isCustomizableItem(child)) {
   918         this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
   919       }
   920     }
   921   },
   923   _removeDragHandlers: function(aTarget) {
   924     aTarget.removeEventListener("dragstart", this, true);
   925     aTarget.removeEventListener("dragover", this, true);
   926     aTarget.removeEventListener("dragexit", this, true);
   927     aTarget.removeEventListener("drop", this, true);
   928     aTarget.removeEventListener("dragend", this, true);
   929   },
   931   _unwrapItemsInArea: function(target) {
   932     for (let toolbarItem of target.children) {
   933       if (this.isWrappedToolbarItem(toolbarItem)) {
   934         this.unwrapToolbarItem(toolbarItem);
   935       }
   936     }
   937   },
   939   _unwrapToolbarItems: function() {
   940     return Task.spawn(function() {
   941       for (let target of this.areas) {
   942         for (let toolbarItem of target.children) {
   943           if (this.isWrappedToolbarItem(toolbarItem)) {
   944             yield this.deferredUnwrapToolbarItem(toolbarItem);
   945           }
   946         }
   947         this._removeDragHandlers(target);
   948       }
   949     }.bind(this)).then(null, ERROR);
   950   },
   952   _removeExtraToolbarsIfEmpty: function() {
   953     let toolbox = this.window.gNavToolbox;
   954     for (let child of toolbox.children) {
   955       if (child.hasAttribute("customindex")) {
   956         let placements = CustomizableUI.getWidgetIdsInArea(child.id);
   957         if (!placements.length) {
   958           CustomizableUI.removeExtraToolbar(child.id);
   959         }
   960       }
   961     }
   962   },
   964   persistCurrentSets: function(aSetBeforePersisting)  {
   965     let document = this.document;
   966     let toolbars = document.querySelectorAll("toolbar[customizable='true'][currentset]");
   967     for (let toolbar of toolbars) {
   968       if (aSetBeforePersisting) {
   969         let set = toolbar.currentSet;
   970         toolbar.setAttribute("currentset", set);
   971       }
   972       // Persist the currentset attribute directly on hardcoded toolbars.
   973       document.persist(toolbar.id, "currentset");
   974     }
   975   },
   977   reset: function() {
   978     this.resetting = true;
   979     // Disable the reset button temporarily while resetting:
   980     let btn = this.document.getElementById("customization-reset-button");
   981     BrowserUITelemetry.countCustomizationEvent("reset");
   982     btn.disabled = true;
   983     return Task.spawn(function() {
   984       this._removePanelCustomizationPlaceholders();
   985       yield this.depopulatePalette();
   986       yield this._unwrapToolbarItems();
   988       CustomizableUI.reset();
   990       yield this._wrapToolbarItems();
   991       this.populatePalette();
   993       this.persistCurrentSets(true);
   995       this._updateResetButton();
   996       this._updateUndoResetButton();
   997       this._updateEmptyPaletteNotice();
   998       this._showPanelCustomizationPlaceholders();
   999       this.resetting = false;
  1000       if (!this._wantToBeInCustomizeMode) {
  1001         this.exit();
  1003     }.bind(this)).then(null, ERROR);
  1004   },
  1006   undoReset: function() {
  1007     this.resetting = true;
  1009     return Task.spawn(function() {
  1010       this._removePanelCustomizationPlaceholders();
  1011       yield this.depopulatePalette();
  1012       yield this._unwrapToolbarItems();
  1014       CustomizableUI.undoReset();
  1016       yield this._wrapToolbarItems();
  1017       this.populatePalette();
  1019       this.persistCurrentSets(true);
  1021       this._updateResetButton();
  1022       this._updateUndoResetButton();
  1023       this._updateEmptyPaletteNotice();
  1024       this.resetting = false;
  1025     }.bind(this)).then(null, ERROR);
  1026   },
  1028   _onToolbarVisibilityChange: function(aEvent) {
  1029     let toolbar = aEvent.target;
  1030     if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
  1031       toolbar.setAttribute("customizing", "true");
  1032     } else {
  1033       toolbar.removeAttribute("customizing");
  1035     this._onUIChange();
  1036   },
  1038   onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
  1039     this._onUIChange();
  1040   },
  1042   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
  1043     this._onUIChange();
  1044   },
  1046   onWidgetRemoved: function(aWidgetId, aArea) {
  1047     this._onUIChange();
  1048   },
  1050   onWidgetBeforeDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
  1051     if (aContainer.ownerDocument.defaultView != this.window || this.resetting) {
  1052       return;
  1054     if (aContainer.id == CustomizableUI.AREA_PANEL) {
  1055       this._removePanelCustomizationPlaceholders();
  1057     // If we get called for widgets that aren't in the window yet, they might not have
  1058     // a parentNode at all.
  1059     if (aNodeToChange.parentNode) {
  1060       this.unwrapToolbarItem(aNodeToChange.parentNode);
  1062     if (aSecondaryNode) {
  1063       this.unwrapToolbarItem(aSecondaryNode.parentNode);
  1065   },
  1067   onWidgetAfterDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
  1068     if (aContainer.ownerDocument.defaultView != this.window || this.resetting) {
  1069       return;
  1071     // If the node is still attached to the container, wrap it again:
  1072     if (aNodeToChange.parentNode) {
  1073       let place = CustomizableUI.getPlaceForItem(aNodeToChange);
  1074       this.wrapToolbarItem(aNodeToChange, place);
  1075       if (aSecondaryNode) {
  1076         this.wrapToolbarItem(aSecondaryNode, place);
  1078     } else {
  1079       // If not, it got removed.
  1081       // If an API-based widget is removed while customizing, append it to the palette.
  1082       // The _applyDrop code itself will take care of positioning it correctly, if
  1083       // applicable. We need the code to be here so removing widgets using CustomizableUI's
  1084       // API also does the right thing (and adds it to the palette)
  1085       let widgetId = aNodeToChange.id;
  1086       let widget = CustomizableUI.getWidget(widgetId);
  1087       if (widget.provider == CustomizableUI.PROVIDER_API) {
  1088         let paletteItem = this.makePaletteItem(widget, "palette");
  1089         this.visiblePalette.appendChild(paletteItem);
  1092     if (aContainer.id == CustomizableUI.AREA_PANEL) {
  1093       this._showPanelCustomizationPlaceholders();
  1095   },
  1097   onWidgetDestroyed: function(aWidgetId) {
  1098     let wrapper = this.document.getElementById("wrapper-" + aWidgetId);
  1099     if (wrapper) {
  1100       let wasInPanel = wrapper.parentNode == this.panelUIContents;
  1101       wrapper.remove();
  1102       if (wasInPanel) {
  1103         this._showPanelCustomizationPlaceholders();
  1106   },
  1108   onWidgetAfterCreation: function(aWidgetId, aArea) {
  1109     // If the node was added to an area, we would have gotten an onWidgetAdded notification,
  1110     // plus associated DOM change notifications, so only do stuff for the palette:
  1111     if (!aArea) {
  1112       let widgetNode = this.document.getElementById(aWidgetId);
  1113       if (widgetNode) {
  1114         this.wrapToolbarItem(widgetNode, "palette");
  1115       } else {
  1116         let widget = CustomizableUI.getWidget(aWidgetId);
  1117         this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
  1120   },
  1122   onAreaNodeRegistered: function(aArea, aContainer) {
  1123     if (aContainer.ownerDocument == this.document) {
  1124       this._wrapItemsInArea(aContainer);
  1125       this._addDragHandlers(aContainer);
  1126       DragPositionManager.add(this.window, aArea, aContainer);
  1127       this.areas.push(aContainer);
  1129   },
  1131   onAreaNodeUnregistered: function(aArea, aContainer, aReason) {
  1132     if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) {
  1133       this._unwrapItemsInArea(aContainer);
  1134       this._removeDragHandlers(aContainer);
  1135       DragPositionManager.remove(this.window, aArea, aContainer);
  1136       let index = this.areas.indexOf(aContainer);
  1137       this.areas.splice(index, 1);
  1139   },
  1141   _onUIChange: function() {
  1142     this._changed = true;
  1143     if (!this.resetting) {
  1144       this._updateResetButton();
  1145       this._updateUndoResetButton();
  1146       this._updateEmptyPaletteNotice();
  1148     CustomizableUI.dispatchToolboxEvent("customizationchange");
  1149   },
  1151   _updateEmptyPaletteNotice: function() {
  1152     let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
  1153     this.paletteEmptyNotice.hidden = !!paletteItems.length;
  1154   },
  1156   _updateResetButton: function() {
  1157     let btn = this.document.getElementById("customization-reset-button");
  1158     btn.disabled = CustomizableUI.inDefaultState;
  1159   },
  1161   _updateUndoResetButton: function() {
  1162     let undoResetButton =  this.document.getElementById("customization-undo-reset-button");
  1163     undoResetButton.hidden = !CustomizableUI.canUndoReset;
  1164   },
  1166   handleEvent: function(aEvent) {
  1167     switch(aEvent.type) {
  1168       case "toolbarvisibilitychange":
  1169         this._onToolbarVisibilityChange(aEvent);
  1170         break;
  1171       case "dragstart":
  1172         this._onDragStart(aEvent);
  1173         break;
  1174       case "dragover":
  1175         this._onDragOver(aEvent);
  1176         break;
  1177       case "drop":
  1178         this._onDragDrop(aEvent);
  1179         break;
  1180       case "dragexit":
  1181         this._onDragExit(aEvent);
  1182         break;
  1183       case "dragend":
  1184         this._onDragEnd(aEvent);
  1185         break;
  1186       case "command":
  1187         if (aEvent.originalTarget == this.window.PanelUI.menuButton) {
  1188           this.exit();
  1189           aEvent.preventDefault();
  1191         break;
  1192       case "mousedown":
  1193         this._onMouseDown(aEvent);
  1194         break;
  1195       case "mouseup":
  1196         this._onMouseUp(aEvent);
  1197         break;
  1198       case "keypress":
  1199         if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
  1200           this.exit();
  1202         break;
  1203 #ifdef CAN_DRAW_IN_TITLEBAR
  1204       case "unload":
  1205         this.uninit();
  1206         break;
  1207 #endif
  1209   },
  1211 #ifdef CAN_DRAW_IN_TITLEBAR
  1212   observe: function(aSubject, aTopic, aData) {
  1213     switch (aTopic) {
  1214       case "nsPref:changed":
  1215         this._updateResetButton();
  1216         this._updateTitlebarButton();
  1217         this._updateUndoResetButton();
  1218         break;
  1220   },
  1222   _updateTitlebarButton: function() {
  1223     let drawInTitlebar = true;
  1224     try {
  1225       drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref);
  1226     } catch (ex) { }
  1227     let button = this.document.getElementById("customization-titlebar-visibility-button");
  1228     // Drawing in the titlebar means 'hiding' the titlebar:
  1229     if (drawInTitlebar) {
  1230       button.removeAttribute("checked");
  1231     } else {
  1232       button.setAttribute("checked", "true");
  1234   },
  1236   toggleTitlebar: function(aShouldShowTitlebar) {
  1237     // Drawing in the titlebar means not showing the titlebar, hence the negation:
  1238     Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
  1239   },
  1240 #endif
  1242   _onDragStart: function(aEvent) {
  1243     __dumpDragData(aEvent);
  1244     let item = aEvent.target;
  1245     while (item && item.localName != "toolbarpaletteitem") {
  1246       if (item.localName == "toolbar") {
  1247         return;
  1249       item = item.parentNode;
  1252     let draggedItem = item.firstChild;
  1253     let placeForItem = CustomizableUI.getPlaceForItem(item);
  1254     let isRemovable = placeForItem == "palette" ||
  1255                       CustomizableUI.isWidgetRemovable(draggedItem);
  1256     if (item.classList.contains(kPlaceholderClass) || !isRemovable) {
  1257       return;
  1260     let dt = aEvent.dataTransfer;
  1261     let documentId = aEvent.target.ownerDocument.documentElement.id;
  1262     let isInToolbar = placeForItem == "toolbar";
  1264     dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
  1265     dt.effectAllowed = "move";
  1267     let itemRect = draggedItem.getBoundingClientRect();
  1268     let itemCenter = {x: itemRect.left + itemRect.width / 2,
  1269                       y: itemRect.top + itemRect.height / 2};
  1270     this._dragOffset = {x: aEvent.clientX - itemCenter.x,
  1271                         y: aEvent.clientY - itemCenter.y};
  1273     gDraggingInToolbars = new Set();
  1275     // Hack needed so that the dragimage will still show the
  1276     // item as it appeared before it was hidden.
  1277     this._initializeDragAfterMove = function() {
  1278       // For automated tests, we sometimes start exiting customization mode
  1279       // before this fires, which leaves us with placeholders inserted after
  1280       // we've exited. So we need to check that we are indeed customizing.
  1281       if (this._customizing && !this._transitioning) {
  1282         item.hidden = true;
  1283         this._showPanelCustomizationPlaceholders();
  1284         DragPositionManager.start(this.window);
  1285         if (item.nextSibling) {
  1286           this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar);
  1287           this._dragOverItem = item.nextSibling;
  1288         } else if (isInToolbar && item.previousSibling) {
  1289           this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar);
  1290           this._dragOverItem = item.previousSibling;
  1293       this._initializeDragAfterMove = null;
  1294       this.window.clearTimeout(this._dragInitializeTimeout);
  1295     }.bind(this);
  1296     this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
  1297   },
  1299   _onDragOver: function(aEvent) {
  1300     if (this._isUnwantedDragDrop(aEvent)) {
  1301       return;
  1303     if (this._initializeDragAfterMove) {
  1304       this._initializeDragAfterMove();
  1307     __dumpDragData(aEvent);
  1309     let document = aEvent.target.ownerDocument;
  1310     let documentId = document.documentElement.id;
  1311     if (!aEvent.dataTransfer.mozTypesAt(0)) {
  1312       return;
  1315     let draggedItemId =
  1316       aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
  1317     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
  1318     let targetArea = this._getCustomizableParent(aEvent.currentTarget);
  1319     let originArea = this._getCustomizableParent(draggedWrapper);
  1321     // Do nothing if the target or origin are not customizable.
  1322     if (!targetArea || !originArea) {
  1323       return;
  1326     // Do nothing if the widget is not allowed to be removed.
  1327     if (targetArea.id == kPaletteId &&
  1328        !CustomizableUI.isWidgetRemovable(draggedItemId)) {
  1329       return;
  1332     // Do nothing if the widget is not allowed to move to the target area.
  1333     if (targetArea.id != kPaletteId &&
  1334         !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
  1335       return;
  1338     let targetIsToolbar = CustomizableUI.getAreaType(targetArea.id) == "toolbar";
  1339     let targetNode = this._getDragOverNode(aEvent, targetArea, targetIsToolbar, draggedItemId);
  1341     // We need to determine the place that the widget is being dropped in
  1342     // the target.
  1343     let dragOverItem, dragValue;
  1344     if (targetNode == targetArea.customizationTarget) {
  1345       // We'll assume if the user is dragging directly over the target, that
  1346       // they're attempting to append a child to that target.
  1347       dragOverItem = (targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
  1348                                         targetNode.lastChild) || targetNode;
  1349       dragValue = "after";
  1350     } else {
  1351       let targetParent = targetNode.parentNode;
  1352       let position = Array.indexOf(targetParent.children, targetNode);
  1353       if (position == -1) {
  1354         dragOverItem = targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
  1355                                          targetParent.lastChild;
  1356         dragValue = "after";
  1357       } else {
  1358         dragOverItem = targetParent.children[position];
  1359         if (!targetIsToolbar) {
  1360           dragValue = "before";
  1361         } else {
  1362           dragOverItem = this._findVisiblePreviousSiblingNode(targetParent.children[position]);
  1363           // Check if the aDraggedItem is hovered past the first half of dragOverItem
  1364           let window = dragOverItem.ownerDocument.defaultView;
  1365           let direction = window.getComputedStyle(dragOverItem, null).direction;
  1366           let itemRect = dragOverItem.getBoundingClientRect();
  1367           let dropTargetCenter = itemRect.left + (itemRect.width / 2);
  1368           let existingDir = dragOverItem.getAttribute("dragover");
  1369           if ((existingDir == "before") == (direction == "ltr")) {
  1370             dropTargetCenter += (parseInt(dragOverItem.style.borderLeftWidth) || 0) / 2;
  1371           } else {
  1372             dropTargetCenter -= (parseInt(dragOverItem.style.borderRightWidth) || 0) / 2;
  1374           let before = direction == "ltr" ? aEvent.clientX < dropTargetCenter : aEvent.clientX > dropTargetCenter;
  1375           dragValue = before ? "before" : "after";
  1380     if (this._dragOverItem && dragOverItem != this._dragOverItem) {
  1381       this._cancelDragActive(this._dragOverItem, dragOverItem);
  1384     if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) {
  1385       if (dragOverItem != targetArea.customizationTarget) {
  1386         this._setDragActive(dragOverItem, dragValue, draggedItemId, targetIsToolbar);
  1387       } else if (targetIsToolbar) {
  1388         this._updateToolbarCustomizationOutline(this.window, targetArea);
  1390       this._dragOverItem = dragOverItem;
  1393     aEvent.preventDefault();
  1394     aEvent.stopPropagation();
  1395   },
  1397   _onDragDrop: function(aEvent) {
  1398     if (this._isUnwantedDragDrop(aEvent)) {
  1399       return;
  1402     __dumpDragData(aEvent);
  1403     this._initializeDragAfterMove = null;
  1404     this.window.clearTimeout(this._dragInitializeTimeout);
  1406     let targetArea = this._getCustomizableParent(aEvent.currentTarget);
  1407     let document = aEvent.target.ownerDocument;
  1408     let documentId = document.documentElement.id;
  1409     let draggedItemId =
  1410       aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
  1411     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
  1412     let originArea = this._getCustomizableParent(draggedWrapper);
  1413     if (this._dragSizeMap) {
  1414       this._dragSizeMap.clear();
  1416     // Do nothing if the target area or origin area are not customizable.
  1417     if (!targetArea || !originArea) {
  1418       return;
  1420     let targetNode = this._dragOverItem;
  1421     let dropDir = targetNode.getAttribute("dragover");
  1422     // Need to insert *after* this node if we promised the user that:
  1423     if (targetNode != targetArea && dropDir == "after") {
  1424       if (targetNode.nextSibling) {
  1425         targetNode = targetNode.nextSibling;
  1426       } else {
  1427         targetNode = targetArea;
  1430     // If the target node is a placeholder, get its sibling as the real target.
  1431     while (targetNode.classList.contains(kPlaceholderClass) && targetNode.nextSibling) {
  1432       targetNode = targetNode.nextSibling;
  1434     if (targetNode.tagName == "toolbarpaletteitem") {
  1435       targetNode = targetNode.firstChild;
  1438     this._cancelDragActive(this._dragOverItem, null, true);
  1439     this._removePanelCustomizationPlaceholders();
  1441     try {
  1442       this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode);
  1443     } catch (ex) {
  1444       ERROR(ex, ex.stack);
  1447     this._showPanelCustomizationPlaceholders();
  1448   },
  1450   _applyDrop: function(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
  1451     let document = aEvent.target.ownerDocument;
  1452     let draggedItem = document.getElementById(aDraggedItemId);
  1453     draggedItem.hidden = false;
  1454     draggedItem.removeAttribute("mousedown");
  1456     // Do nothing if the target was dropped onto itself (ie, no change in area
  1457     // or position).
  1458     if (draggedItem == aTargetNode) {
  1459       return;
  1462     // Is the target area the customization palette?
  1463     if (aTargetArea.id == kPaletteId) {
  1464       // Did we drag from outside the palette?
  1465       if (aOriginArea.id !== kPaletteId) {
  1466         if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) {
  1467           return;
  1470         CustomizableUI.removeWidgetFromArea(aDraggedItemId);
  1471         BrowserUITelemetry.countCustomizationEvent("remove");
  1472         // Special widgets are removed outright, we can return here:
  1473         if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
  1474           return;
  1477       draggedItem = draggedItem.parentNode;
  1479       // If the target node is the palette itself, just append
  1480       if (aTargetNode == this.visiblePalette) {
  1481         this.visiblePalette.appendChild(draggedItem);
  1482       } else {
  1483         // The items in the palette are wrapped, so we need the target node's parent here:
  1484         this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
  1486       if (aOriginArea.id !== kPaletteId) {
  1487         // The dragend event already fires when the item moves within the palette.
  1488         this._onDragEnd(aEvent);
  1490       return;
  1493     if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
  1494       return;
  1497     // Skipintoolbarset items won't really be moved:
  1498     if (draggedItem.getAttribute("skipintoolbarset") == "true") {
  1499       // These items should never leave their area:
  1500       if (aTargetArea != aOriginArea) {
  1501         return;
  1503       let place = draggedItem.parentNode.getAttribute("place");
  1504       this.unwrapToolbarItem(draggedItem.parentNode);
  1505       if (aTargetNode == aTargetArea.customizationTarget) {
  1506         aTargetArea.customizationTarget.appendChild(draggedItem);
  1507       } else {
  1508         this.unwrapToolbarItem(aTargetNode.parentNode);
  1509         aTargetArea.customizationTarget.insertBefore(draggedItem, aTargetNode);
  1510         this.wrapToolbarItem(aTargetNode, place);
  1512       this.wrapToolbarItem(draggedItem, place);
  1513       BrowserUITelemetry.countCustomizationEvent("move");
  1514       return;
  1517     // Is the target the customization area itself? If so, we just add the
  1518     // widget to the end of the area.
  1519     if (aTargetNode == aTargetArea.customizationTarget) {
  1520       CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
  1521       // For the purposes of BrowserUITelemetry, we consider both moving a widget
  1522       // within the same area, and adding a widget from one area to another area
  1523       // as a "move". An "add" is only when we move an item from the palette into
  1524       // an area.
  1525       let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
  1526       BrowserUITelemetry.countCustomizationEvent(custEventType);
  1527       this._onDragEnd(aEvent);
  1528       return;
  1531     // We need to determine the place that the widget is being dropped in
  1532     // the target.
  1533     let placement;
  1534     let itemForPlacement = aTargetNode;
  1535     // Skip the skipintoolbarset items when determining the place of the item:
  1536     while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
  1537            itemForPlacement.parentNode &&
  1538            itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") {
  1539       itemForPlacement = itemForPlacement.parentNode.nextSibling;
  1540       if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") {
  1541         itemForPlacement = itemForPlacement.firstChild;
  1544     if (itemForPlacement && !itemForPlacement.classList.contains(kPlaceholderClass)) {
  1545       let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ?
  1546                             itemForPlacement.firstChild && itemForPlacement.firstChild.id :
  1547                             itemForPlacement.id;
  1548       placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
  1550     if (!placement) {
  1551       LOG("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className);
  1553     let position = placement ? placement.position : null;
  1555     // Is the target area the same as the origin? Since we've already handled
  1556     // the possibility that the target is the customization palette, we know
  1557     // that the widget is moving within a customizable area.
  1558     if (aTargetArea == aOriginArea) {
  1559       CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position);
  1560     } else {
  1561       CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
  1564     this._onDragEnd(aEvent);
  1566     // For BrowserUITelemetry, an "add" is only when we move an item from the palette
  1567     // into an area. Otherwise, it's a move.
  1568     let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
  1569     BrowserUITelemetry.countCustomizationEvent(custEventType);
  1571     // If we dropped onto a skipintoolbarset item, manually correct the drop location:
  1572     if (aTargetNode != itemForPlacement) {
  1573       let draggedWrapper = draggedItem.parentNode;
  1574       let container = draggedWrapper.parentNode;
  1575       container.insertBefore(draggedWrapper, aTargetNode.parentNode);
  1577   },
  1579   _onDragExit: function(aEvent) {
  1580     if (this._isUnwantedDragDrop(aEvent)) {
  1581       return;
  1584     __dumpDragData(aEvent);
  1586     // When leaving customization areas, cancel the drag on the last dragover item
  1587     // We've attached the listener to areas, so aEvent.currentTarget will be the area.
  1588     // We don't care about dragexit events fired on descendants of the area,
  1589     // so we check that the event's target is the same as the area to which the listener
  1590     // was attached.
  1591     if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
  1592       this._cancelDragActive(this._dragOverItem);
  1593       this._dragOverItem = null;
  1595   },
  1597   /**
  1598    * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
  1599    */
  1600   _onDragEnd: function(aEvent) {
  1601     if (this._isUnwantedDragDrop(aEvent)) {
  1602       return;
  1604     this._initializeDragAfterMove = null;
  1605     this.window.clearTimeout(this._dragInitializeTimeout);
  1606     __dumpDragData(aEvent, "_onDragEnd");
  1608     let document = aEvent.target.ownerDocument;
  1609     document.documentElement.removeAttribute("customizing-movingItem");
  1611     let documentId = document.documentElement.id;
  1612     if (!aEvent.dataTransfer.mozTypesAt(0)) {
  1613       return;
  1616     let draggedItemId =
  1617       aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
  1619     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
  1620     draggedWrapper.hidden = false;
  1621     draggedWrapper.removeAttribute("mousedown");
  1622     if (this._dragOverItem) {
  1623       this._cancelDragActive(this._dragOverItem);
  1624       this._dragOverItem = null;
  1626     this._updateToolbarCustomizationOutline(this.window);
  1627     this._showPanelCustomizationPlaceholders();
  1628     DragPositionManager.stop();
  1629   },
  1631   _isUnwantedDragDrop: function(aEvent) {
  1632     // The simulated events generated by synthesizeDragStart/synthesizeDrop in
  1633     // mochitests are used only for testing whether the right data is being put
  1634     // into the dataTransfer. Neither cause a real drop to occur, so they don't
  1635     // set the source node. There isn't a means of testing real drag and drops,
  1636     // so this pref skips the check but it should only be set by test code.
  1637     if (this._skipSourceNodeCheck) {
  1638       return false;
  1641     /* Discard drag events that originated from a separate window to
  1642        prevent content->chrome privilege escalations. */
  1643     let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
  1644     // mozSourceNode is null in the dragStart event handler or if
  1645     // the drag event originated in an external application.
  1646     return !mozSourceNode ||
  1647            mozSourceNode.ownerDocument.defaultView != this.window;
  1648   },
  1650   _setDragActive: function(aItem, aValue, aDraggedItemId, aInToolbar) {
  1651     if (!aItem) {
  1652       return;
  1655     if (aItem.getAttribute("dragover") != aValue) {
  1656       aItem.setAttribute("dragover", aValue);
  1658       let window = aItem.ownerDocument.defaultView;
  1659       let draggedItem = window.document.getElementById(aDraggedItemId);
  1660       if (!aInToolbar) {
  1661         this._setGridDragActive(aItem, draggedItem, aValue);
  1662       } else {
  1663         let targetArea = this._getCustomizableParent(aItem);
  1664         this._updateToolbarCustomizationOutline(window, targetArea);
  1665         let makeSpaceImmediately = false;
  1666         if (!gDraggingInToolbars.has(targetArea.id)) {
  1667           gDraggingInToolbars.add(targetArea.id);
  1668           let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItemId);
  1669           let originArea = this._getCustomizableParent(draggedWrapper);
  1670           makeSpaceImmediately = originArea == targetArea;
  1672         // Calculate width of the item when it'd be dropped in this position
  1673         let width = this._getDragItemSize(aItem, draggedItem).width;
  1674         let direction = window.getComputedStyle(aItem).direction;
  1675         let prop, otherProp;
  1676         // If we're inserting before in ltr, or after in rtl:
  1677         if ((aValue == "before") == (direction == "ltr")) {
  1678           prop = "borderLeftWidth";
  1679           otherProp = "border-right-width";
  1680         } else {
  1681           // otherwise:
  1682           prop = "borderRightWidth";
  1683           otherProp = "border-left-width";
  1685         if (makeSpaceImmediately) {
  1686           aItem.setAttribute("notransition", "true");
  1688         aItem.style[prop] = width + 'px';
  1689         aItem.style.removeProperty(otherProp);
  1690         if (makeSpaceImmediately) {
  1691           // Force a layout flush:
  1692           aItem.getBoundingClientRect();
  1693           aItem.removeAttribute("notransition");
  1697   },
  1698   _cancelDragActive: function(aItem, aNextItem, aNoTransition) {
  1699     this._updateToolbarCustomizationOutline(aItem.ownerDocument.defaultView);
  1700     let currentArea = this._getCustomizableParent(aItem);
  1701     if (!currentArea) {
  1702       return;
  1704     let isToolbar = CustomizableUI.getAreaType(currentArea.id) == "toolbar";
  1705     if (isToolbar) {
  1706       if (aNoTransition) {
  1707         aItem.setAttribute("notransition", "true");
  1709       aItem.removeAttribute("dragover");
  1710       // Remove both property values in the case that the end padding
  1711       // had been set.
  1712       aItem.style.removeProperty("border-left-width");
  1713       aItem.style.removeProperty("border-right-width");
  1714       if (aNoTransition) {
  1715         // Force a layout flush:
  1716         aItem.getBoundingClientRect();
  1717         aItem.removeAttribute("notransition");
  1719     } else  {
  1720       aItem.removeAttribute("dragover");
  1721       if (aNextItem) {
  1722         let nextArea = this._getCustomizableParent(aNextItem);
  1723         if (nextArea == currentArea) {
  1724           // No need to do anything if we're still dragging in this area:
  1725           return;
  1728       // Otherwise, clear everything out:
  1729       let positionManager = DragPositionManager.getManagerForArea(currentArea);
  1730       positionManager.clearPlaceholders(currentArea, aNoTransition);
  1732   },
  1734   _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) {
  1735     let targetArea = this._getCustomizableParent(aDragOverNode);
  1736     let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItem.id);
  1737     let originArea = this._getCustomizableParent(draggedWrapper);
  1738     let positionManager = DragPositionManager.getManagerForArea(targetArea);
  1739     let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
  1740     let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
  1741     positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize,
  1742                                       originArea == targetArea);
  1743   },
  1745   _getDragItemSize: function(aDragOverNode, aDraggedItem) {
  1746     // Cache it good, cache it real good.
  1747     if (!this._dragSizeMap)
  1748       this._dragSizeMap = new WeakMap();
  1749     if (!this._dragSizeMap.has(aDraggedItem))
  1750       this._dragSizeMap.set(aDraggedItem, new WeakMap());
  1751     let itemMap = this._dragSizeMap.get(aDraggedItem);
  1752     let targetArea = this._getCustomizableParent(aDragOverNode);
  1753     let currentArea = this._getCustomizableParent(aDraggedItem);
  1754     // Return the size for this target from cache, if it exists.
  1755     let size = itemMap.get(targetArea);
  1756     if (size)
  1757       return size;
  1759     // Calculate size of the item when it'd be dropped in this position.
  1760     let currentParent = aDraggedItem.parentNode;
  1761     let currentSibling = aDraggedItem.nextSibling;
  1762     const kAreaType = "cui-areatype";
  1763     let areaType, currentType;
  1765     if (targetArea != currentArea) {
  1766       // Move the widget temporarily next to the placeholder.
  1767       aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
  1768       // Update the node's areaType.
  1769       areaType = CustomizableUI.getAreaType(targetArea.id);
  1770       currentType = aDraggedItem.hasAttribute(kAreaType) &&
  1771                     aDraggedItem.getAttribute(kAreaType);
  1772       if (areaType)
  1773         aDraggedItem.setAttribute(kAreaType, areaType);
  1774       this.wrapToolbarItem(aDraggedItem, areaType || "palette");
  1775       CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
  1776     } else {
  1777       aDraggedItem.parentNode.hidden = false;
  1780     // Fetch the new size.
  1781     let rect = aDraggedItem.parentNode.getBoundingClientRect();
  1782     size = {width: rect.width, height: rect.height};
  1783     // Cache the found value of size for this target.
  1784     itemMap.set(targetArea, size);
  1786     if (targetArea != currentArea) {
  1787       this.unwrapToolbarItem(aDraggedItem.parentNode);
  1788       // Put the item back into its previous position.
  1789       currentParent.insertBefore(aDraggedItem, currentSibling);
  1790       // restore the areaType
  1791       if (areaType) {
  1792         if (currentType === false)
  1793           aDraggedItem.removeAttribute(kAreaType);
  1794         else
  1795           aDraggedItem.setAttribute(kAreaType, currentType);
  1797       this.createOrUpdateWrapper(aDraggedItem, null, true);
  1798       CustomizableUI.onWidgetDrag(aDraggedItem.id);
  1799     } else {
  1800       aDraggedItem.parentNode.hidden = true;
  1802     return size;
  1803   },
  1805   _getCustomizableParent: function(aElement) {
  1806     let areas = CustomizableUI.areas;
  1807     areas.push(kPaletteId);
  1808     while (aElement) {
  1809       if (areas.indexOf(aElement.id) != -1) {
  1810         return aElement;
  1812       aElement = aElement.parentNode;
  1814     return null;
  1815   },
  1817   _getDragOverNode: function(aEvent, aAreaElement, aInToolbar, aDraggedItemId) {
  1818     let expectedParent = aAreaElement.customizationTarget || aAreaElement;
  1819     // Our tests are stupid. Cope:
  1820     if (!aEvent.clientX  && !aEvent.clientY) {
  1821       return aEvent.target;
  1823     // Offset the drag event's position with the offset to the center of
  1824     // the thing we're dragging
  1825     let dragX = aEvent.clientX - this._dragOffset.x;
  1826     let dragY = aEvent.clientY - this._dragOffset.y;
  1828     // Ensure this is within the container
  1829     let boundsContainer = expectedParent;
  1830     // NB: because the panel UI itself is inside a scrolling container, we need
  1831     // to use the parent bounds (otherwise, if the panel UI is scrolled down,
  1832     // the numbers we get are in window coordinates which leads to various kinds
  1833     // of weirdness)
  1834     if (boundsContainer == this.panelUIContents) {
  1835       boundsContainer = boundsContainer.parentNode;
  1837     let bounds = boundsContainer.getBoundingClientRect();
  1838     dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
  1839     dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
  1841     let targetNode;
  1842     if (aInToolbar) {
  1843       targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
  1844       while (targetNode && targetNode.parentNode != expectedParent) {
  1845         targetNode = targetNode.parentNode;
  1847     } else {
  1848       let positionManager = DragPositionManager.getManagerForArea(aAreaElement);
  1849       // Make it relative to the container:
  1850       dragX -= bounds.left;
  1851       // NB: but if we're in the panel UI, we need to use the actual panel
  1852       // contents instead of the scrolling container to determine our origin
  1853       // offset against:
  1854       if (expectedParent == this.panelUIContents) {
  1855         dragY -= this.panelUIContents.getBoundingClientRect().top;
  1856       } else {
  1857         dragY -= bounds.top;
  1859       // Find the closest node:
  1860       targetNode = positionManager.find(aAreaElement, dragX, dragY, aDraggedItemId);
  1862     return targetNode || aEvent.target;
  1863   },
  1865   _onMouseDown: function(aEvent) {
  1866     LOG("_onMouseDown");
  1867     if (aEvent.button != 0) {
  1868       return;
  1870     let doc = aEvent.target.ownerDocument;
  1871     doc.documentElement.setAttribute("customizing-movingItem", true);
  1872     let item = this._getWrapper(aEvent.target);
  1873     if (item && !item.classList.contains(kPlaceholderClass) &&
  1874         item.getAttribute("removable") == "true") {
  1875       item.setAttribute("mousedown", "true");
  1877   },
  1879   _onMouseUp: function(aEvent) {
  1880     LOG("_onMouseUp");
  1881     if (aEvent.button != 0) {
  1882       return;
  1884     let doc = aEvent.target.ownerDocument;
  1885     doc.documentElement.removeAttribute("customizing-movingItem");
  1886     let item = this._getWrapper(aEvent.target);
  1887     if (item) {
  1888       item.removeAttribute("mousedown");
  1890   },
  1892   _getWrapper: function(aElement) {
  1893     while (aElement && aElement.localName != "toolbarpaletteitem") {
  1894       if (aElement.localName == "toolbar")
  1895         return null;
  1896       aElement = aElement.parentNode;
  1898     return aElement;
  1899   },
  1901   _showPanelCustomizationPlaceholders: function() {
  1902     let doc = this.document;
  1903     let contents = this.panelUIContents;
  1904     let narrowItemsAfterWideItem = 0;
  1905     let node = contents.lastChild;
  1906     while (node && !node.classList.contains(CustomizableUI.WIDE_PANEL_CLASS) &&
  1907            (!node.firstChild || !node.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS))) {
  1908       if (!node.hidden && !node.classList.contains(kPlaceholderClass)) {
  1909         narrowItemsAfterWideItem++;
  1911       node = node.previousSibling;
  1914     let orphanedItems = narrowItemsAfterWideItem % CustomizableUI.PANEL_COLUMN_COUNT;
  1915     let placeholders = CustomizableUI.PANEL_COLUMN_COUNT - orphanedItems;
  1917     let currentPlaceholderCount = contents.querySelectorAll("." + kPlaceholderClass).length;
  1918     if (placeholders > currentPlaceholderCount) {
  1919       while (placeholders-- > currentPlaceholderCount) {
  1920         let placeholder = doc.createElement("toolbarpaletteitem");
  1921         placeholder.classList.add(kPlaceholderClass);
  1922         //XXXjaws The toolbarbutton child here is only necessary to get
  1923         //  the styling right here.
  1924         let placeholderChild = doc.createElement("toolbarbutton");
  1925         placeholderChild.classList.add(kPlaceholderClass + "-child");
  1926         placeholder.appendChild(placeholderChild);
  1927         contents.appendChild(placeholder);
  1929     } else if (placeholders < currentPlaceholderCount) {
  1930       while (placeholders++ < currentPlaceholderCount) {
  1931         contents.querySelectorAll("." + kPlaceholderClass)[0].remove();
  1934   },
  1936   _removePanelCustomizationPlaceholders: function() {
  1937     let contents = this.panelUIContents;
  1938     let oldPlaceholders = contents.getElementsByClassName(kPlaceholderClass);
  1939     while (oldPlaceholders.length) {
  1940       contents.removeChild(oldPlaceholders[0]);
  1942   },
  1944   /**
  1945    * Update toolbar customization targets during drag events to add or remove
  1946    * outlines to indicate that an area is customizable.
  1948    * @param aWindow                       The XUL window in which outlines should be updated.
  1949    * @param {Element} [aToolbarArea=null] The element of the customizable toolbar area to add the
  1950    *                                      outline to. If aToolbarArea is falsy, the outline will be
  1951    *                                      removed from all toolbar areas.
  1952    */
  1953   _updateToolbarCustomizationOutline: function(aWindow, aToolbarArea = null) {
  1954     // Remove the attribute from existing customization targets
  1955     for (let area of CustomizableUI.areas) {
  1956       if (CustomizableUI.getAreaType(area) != CustomizableUI.TYPE_TOOLBAR) {
  1957         continue;
  1959       let target = CustomizableUI.getCustomizeTargetForArea(area, aWindow);
  1960       target.removeAttribute("customizing-dragovertarget");
  1963     // Now set the attribute on the desired target
  1964     if (aToolbarArea) {
  1965       if (CustomizableUI.getAreaType(aToolbarArea.id) != CustomizableUI.TYPE_TOOLBAR)
  1966         return;
  1967       let target = CustomizableUI.getCustomizeTargetForArea(aToolbarArea.id, aWindow);
  1968       target.setAttribute("customizing-dragovertarget", true);
  1970   },
  1972   _findVisiblePreviousSiblingNode: function(aReferenceNode) {
  1973     while (aReferenceNode &&
  1974            aReferenceNode.localName == "toolbarpaletteitem" &&
  1975            aReferenceNode.firstChild.hidden) {
  1976       aReferenceNode = aReferenceNode.previousSibling;
  1978     return aReferenceNode;
  1979   },
  1980 };
  1982 function __dumpDragData(aEvent, caller) {
  1983   if (!gDebug) {
  1984     return;
  1986   let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
  1987   str += "  type: " + aEvent["type"] + "\n";
  1988   for (let el of ["target", "currentTarget", "relatedTarget"]) {
  1989     if (aEvent[el]) {
  1990       str += "  " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n";
  1993   for (let prop in aEvent.dataTransfer) {
  1994     if (typeof aEvent.dataTransfer[prop] != "function") {
  1995       str += "  dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
  1998   str += "}";
  1999   LOG(str);
  2002 function dispatchFunction(aFunc) {
  2003   Services.tm.currentThread.dispatch(aFunc, Ci.nsIThread.DISPATCH_NORMAL);

mercurial