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/. */
4
5"use strict";
6
7this.EXPORTED_SYMBOLS = ["CustomizeMode"];
8
9const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
10
11const kPrefCustomizationDebug = "browser.uiCustomization.debug";
12const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation";
13const kPaletteId = "customization-palette";
14const kDragDataTypePrefix = "text/toolbarwrapper-id/";
15const kPlaceholderClass = "panel-customization-placeholder";
16const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
17const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
18const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
19const kMaxTransitionDurationMs = 2000;
20
21const kPanelItemContextMenu = "customizationPanelItemContextMenu";
22const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
23
24Cu.import("resource://gre/modules/Services.jsm");
25Cu.import("resource:///modules/CustomizableUI.jsm");
26Cu.import("resource://gre/modules/XPCOMUtils.jsm");
27Cu.import("resource://gre/modules/Task.jsm");
28Cu.import("resource://gre/modules/Promise.jsm");
29Cu.import("resource://gre/modules/AddonManager.jsm");
30Cu.import("resource://gre/modules/AppConstants.jsm");
31
32XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager",
33                                  "resource:///modules/DragPositionManager.jsm");
34XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
35                                  "resource:///modules/BrowserUITelemetry.jsm");
36XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
37                                  "resource://gre/modules/LightweightThemeManager.jsm");
38XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
39                                  "resource:///modules/sessionstore/SessionStore.jsm");
40
41let gDebug;
42XPCOMUtils.defineLazyGetter(this, "log", () => {
43  let scope = {};
44  Cu.import("resource://gre/modules/Console.jsm", scope);
45  try {
46    gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug);
47  } catch (ex) {}
48  let consoleOptions = {
49    maxLogLevel: gDebug ? "all" : "log",
50    prefix: "CustomizeMode",
51  };
52  return new scope.ConsoleAPI(consoleOptions);
53});
54
55var gDisableAnimation = null;
56
57var gDraggingInToolbars;
58
59var gTab;
60
61function closeGlobalTab() {
62  let win = gTab.ownerGlobal;
63  if (win.gBrowser.browsers.length == 1) {
64    win.BrowserOpenTab();
65  }
66  win.gBrowser.removeTab(gTab);
67  gTab = null;
68}
69
70function unregisterGlobalTab() {
71  gTab.removeEventListener("TabClose", unregisterGlobalTab);
72  gTab.ownerGlobal.removeEventListener("unload", unregisterGlobalTab);
73  gTab.removeAttribute("customizemode");
74  gTab = null;
75}
76
77function CustomizeMode(aWindow) {
78  if (gDisableAnimation === null) {
79    gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL &&
80                        Services.prefs.getBoolPref(kPrefCustomizationAnimation);
81  }
82  this.window = aWindow;
83  this.document = aWindow.document;
84  this.browser = aWindow.gBrowser;
85  this.areas = new Set();
86
87  // There are two palettes - there's the palette that can be overlayed with
88  // toolbar items in browser.xul. This is invisible, and never seen by the
89  // user. Then there's the visible palette, which gets populated and displayed
90  // to the user when in customizing mode.
91  this.visiblePalette = this.document.getElementById(kPaletteId);
92  this.paletteEmptyNotice = this.document.getElementById("customization-empty");
93  this.tipPanel = this.document.getElementById("customization-tipPanel");
94  if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") {
95    let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
96    lwthemeButton.setAttribute("hidden", "true");
97  }
98  if (AppConstants.CAN_DRAW_IN_TITLEBAR) {
99    this._updateTitlebarButton();
100    Services.prefs.addObserver(kDrawInTitlebarPref, this, false);
101  }
102  this.window.addEventListener("unload", this);
103}
104
105CustomizeMode.prototype = {
106  _changed: false,
107  _transitioning: false,
108  window: null,
109  document: null,
110  // areas is used to cache the customizable areas when in customization mode.
111  areas: null,
112  // When in customizing mode, we swap out the reference to the invisible
113  // palette in gNavToolbox.palette for our visiblePalette. This way, for the
114  // customizing browser window, when widgets are removed from customizable
115  // areas and added to the palette, they're added to the visible palette.
116  // _stowedPalette is a reference to the old invisible palette so we can
117  // restore gNavToolbox.palette to its original state after exiting
118  // customization mode.
119  _stowedPalette: null,
120  _dragOverItem: null,
121  _customizing: false,
122  _skipSourceNodeCheck: null,
123  _mainViewContext: null,
124
125  get panelUIContents() {
126    return this.document.getElementById("PanelUI-contents");
127  },
128
129  get _handler() {
130    return this.window.CustomizationHandler;
131  },
132
133  uninit: function() {
134    if (AppConstants.CAN_DRAW_IN_TITLEBAR) {
135      Services.prefs.removeObserver(kDrawInTitlebarPref, this);
136    }
137  },
138
139  toggle: function() {
140    if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
141      this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
142      return;
143    }
144    if (this._customizing) {
145      this.exit();
146    } else {
147      this.enter();
148    }
149  },
150
151  _updateLWThemeButtonIcon: function() {
152    let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
153    let lwthemeIcon = this.document.getAnonymousElementByAttribute(lwthemeButton,
154                        "class", "button-icon");
155    lwthemeIcon.style.backgroundImage = LightweightThemeManager.currentTheme ?
156      "url(" + LightweightThemeManager.currentTheme.iconURL + ")" : "";
157  },
158
159  setTab: function(aTab) {
160    if (gTab == aTab) {
161      return;
162    }
163
164    if (gTab) {
165      closeGlobalTab();
166    }
167
168    gTab = aTab;
169
170    gTab.setAttribute("customizemode", "true");
171    SessionStore.persistTabAttribute("customizemode");
172
173    gTab.linkedBrowser.stop();
174
175    let win = gTab.ownerGlobal;
176
177    win.gBrowser.setTabTitle(gTab);
178    win.gBrowser.setIcon(gTab,
179                         "chrome://browser/skin/customizableui/customizeFavicon.ico");
180
181    gTab.addEventListener("TabClose", unregisterGlobalTab);
182    win.addEventListener("unload", unregisterGlobalTab);
183
184    if (gTab.selected) {
185      win.gCustomizeMode.enter();
186    }
187  },
188
189  enter: function() {
190    this._wantToBeInCustomizeMode = true;
191
192    if (this._customizing || this._handler.isEnteringCustomizeMode) {
193      return;
194    }
195
196    // Exiting; want to re-enter once we've done that.
197    if (this._handler.isExitingCustomizeMode) {
198      log.debug("Attempted to enter while we're in the middle of exiting. " +
199                "We'll exit after we've entered");
200      return;
201    }
202
203    if (!gTab) {
204      this.setTab(this.browser.loadOneTab("about:blank",
205                                          { inBackground: false,
206                                            forceNotRemote: true,
207                                            skipAnimation: true }));
208      return;
209    }
210    if (!gTab.selected) {
211      // This will force another .enter() to be called via the
212      // onlocationchange handler of the tabbrowser, so we return early.
213      gTab.ownerGlobal.gBrowser.selectedTab = gTab;
214      return;
215    }
216    gTab.ownerGlobal.focus();
217    if (gTab.ownerDocument != this.document) {
218      return;
219    }
220
221    let window = this.window;
222    let document = this.document;
223
224    this._handler.isEnteringCustomizeMode = true;
225
226    // Always disable the reset button at the start of customize mode, it'll be re-enabled
227    // if necessary when we finish entering:
228    let resetButton = this.document.getElementById("customization-reset-button");
229    resetButton.setAttribute("disabled", "true");
230
231    Task.spawn(function*() {
232      // We shouldn't start customize mode until after browser-delayed-startup has finished:
233      if (!this.window.gBrowserInit.delayedStartupFinished) {
234        yield new Promise(resolve => {
235          let delayedStartupObserver = aSubject => {
236            if (aSubject == this.window) {
237              Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
238              resolve();
239            }
240          };
241
242          Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
243        });
244      }
245
246      let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
247      let togglableToolbars = window.getTogglableToolbars();
248      if (togglableToolbars.length == 0) {
249        toolbarVisibilityBtn.setAttribute("hidden", "true");
250      } else {
251        toolbarVisibilityBtn.removeAttribute("hidden");
252      }
253
254      this.updateLWTStyling();
255
256      CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
257      CustomizableUI.notifyStartCustomizing(this.window);
258
259      // Add a keypress listener to the document so that we can quickly exit
260      // customization mode when pressing ESC.
261      document.addEventListener("keypress", this);
262
263      // Same goes for the menu button - if we're customizing, a click on the
264      // menu button means a quick exit from customization mode.
265      window.PanelUI.hide();
266      window.PanelUI.menuButton.addEventListener("command", this);
267      window.PanelUI.menuButton.open = true;
268      window.PanelUI.beginBatchUpdate();
269
270      // The menu panel is lazy, and registers itself when the popup shows. We
271      // need to force the menu panel to register itself, or else customization
272      // is really not going to work. We pass "true" to ensureReady to
273      // indicate that we're handling calling startBatchUpdate and
274      // endBatchUpdate.
275      if (!window.PanelUI.isReady) {
276        yield window.PanelUI.ensureReady(true);
277      }
278
279      // Hide the palette before starting the transition for increased perf.
280      this.visiblePalette.hidden = true;
281      this.visiblePalette.removeAttribute("showing");
282
283      // Disable the button-text fade-out mask
284      // during the transition for increased perf.
285      let panelContents = window.PanelUI.contents;
286      panelContents.setAttribute("customize-transitioning", "true");
287
288      // Move the mainView in the panel to the holder so that we can see it
289      // while customizing.
290      let mainView = window.PanelUI.mainView;
291      let panelHolder = document.getElementById("customization-panelHolder");
292      panelHolder.appendChild(mainView);
293
294      let customizeButton = document.getElementById("PanelUI-customize");
295      customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label"));
296      customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel"));
297      customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext"));
298      customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext"));
299
300      this._transitioning = true;
301
302      let customizer = document.getElementById("customization-container");
303      customizer.parentNode.selectedPanel = customizer;
304      customizer.hidden = false;
305
306      this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP);
307
308      let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
309      for (let toolbar of customizableToolbars)
310        toolbar.setAttribute("customizing", true);
311
312      yield this._doTransition(true);
313
314      Services.obs.addObserver(this, "lightweight-theme-window-updated", false);
315
316      // Let everybody in this window know that we're about to customize.
317      CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
318
319      this._mainViewContext = mainView.getAttribute("context");
320      if (this._mainViewContext) {
321        mainView.removeAttribute("context");
322      }
323
324      this._showPanelCustomizationPlaceholders();
325
326      yield this._wrapToolbarItems();
327      this.populatePalette();
328
329      this._addDragHandlers(this.visiblePalette);
330
331      window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
332
333      document.getElementById("PanelUI-help").setAttribute("disabled", true);
334      document.getElementById("PanelUI-quit").setAttribute("disabled", true);
335
336      this._updateResetButton();
337      this._updateUndoResetButton();
338
339      this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
340                                  Services.prefs.getBoolPref(kSkipSourceNodePref);
341
342      CustomizableUI.addListener(this);
343      window.PanelUI.endBatchUpdate();
344      this._customizing = true;
345      this._transitioning = false;
346
347      // Show the palette now that the transition has finished.
348      this.visiblePalette.hidden = false;
349      window.setTimeout(() => {
350        // Force layout reflow to ensure the animation runs,
351        // and make it async so it doesn't affect the timing.
352        this.visiblePalette.clientTop;
353        this.visiblePalette.setAttribute("showing", "true");
354      }, 0);
355      this._updateEmptyPaletteNotice();
356
357      this._updateLWThemeButtonIcon();
358      this.maybeShowTip(panelHolder);
359
360      this._handler.isEnteringCustomizeMode = false;
361      panelContents.removeAttribute("customize-transitioning");
362
363      CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
364      this._enableOutlinesTimeout = window.setTimeout(() => {
365        this.document.getElementById("nav-bar").setAttribute("showoutline", "true");
366        this.panelUIContents.setAttribute("showoutline", "true");
367        delete this._enableOutlinesTimeout;
368      }, 0);
369
370      if (!this._wantToBeInCustomizeMode) {
371        this.exit();
372      }
373    }.bind(this)).then(null, function(e) {
374      log.error("Error entering customize mode", e);
375      // We should ensure this has been called, and calling it again doesn't hurt:
376      window.PanelUI.endBatchUpdate();
377      this._handler.isEnteringCustomizeMode = false;
378      // Exit customize mode to ensure proper clean-up when entering failed.
379      this.exit();
380    }.bind(this));
381  },
382
383  exit: function() {
384    this._wantToBeInCustomizeMode = false;
385
386    if (!this._customizing || this._handler.isExitingCustomizeMode) {
387      return;
388    }
389
390    // Entering; want to exit once we've done that.
391    if (this._handler.isEnteringCustomizeMode) {
392      log.debug("Attempted to exit while we're in the middle of entering. " +
393                "We'll exit after we've entered");
394      return;
395    }
396
397    if (this.resetting) {
398      log.debug("Attempted to exit while we're resetting. " +
399                "We'll exit after resetting has finished.");
400      return;
401    }
402
403    this.hideTip();
404
405    this._handler.isExitingCustomizeMode = true;
406
407    if (this._enableOutlinesTimeout) {
408      this.window.clearTimeout(this._enableOutlinesTimeout);
409    } else {
410      this.document.getElementById("nav-bar").removeAttribute("showoutline");
411      this.panelUIContents.removeAttribute("showoutline");
412    }
413
414    this._removeExtraToolbarsIfEmpty();
415
416    CustomizableUI.removeListener(this);
417
418    this.document.removeEventListener("keypress", this);
419    this.window.PanelUI.menuButton.removeEventListener("command", this);
420    this.window.PanelUI.menuButton.open = false;
421
422    this.window.PanelUI.beginBatchUpdate();
423
424    this._removePanelCustomizationPlaceholders();
425
426    let window = this.window;
427    let document = this.document;
428
429    // Hide the palette before starting the transition for increased perf.
430    this.visiblePalette.hidden = true;
431    this.visiblePalette.removeAttribute("showing");
432    this.paletteEmptyNotice.hidden = true;
433
434    // Disable the button-text fade-out mask
435    // during the transition for increased perf.
436    let panelContents = window.PanelUI.contents;
437    panelContents.setAttribute("customize-transitioning", "true");
438
439    // Disable the reset and undo reset buttons while transitioning:
440    let resetButton = this.document.getElementById("customization-reset-button");
441    let undoResetButton = this.document.getElementById("customization-undo-reset-button");
442    undoResetButton.hidden = resetButton.disabled = true;
443
444    this._transitioning = true;
445
446    Task.spawn(function*() {
447      yield this.depopulatePalette();
448
449      yield this._doTransition(false);
450      this.removeLWTStyling();
451
452      Services.obs.removeObserver(this, "lightweight-theme-window-updated", false);
453
454      if (this.browser.selectedTab == gTab) {
455        if (gTab.linkedBrowser.currentURI.spec == "about:blank") {
456          closeGlobalTab();
457        } else {
458          unregisterGlobalTab();
459        }
460      }
461      let browser = document.getElementById("browser");
462      browser.parentNode.selectedPanel = browser;
463      let customizer = document.getElementById("customization-container");
464      customizer.hidden = true;
465
466      window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
467
468      DragPositionManager.stop();
469      this._removeDragHandlers(this.visiblePalette);
470
471      yield this._unwrapToolbarItems();
472
473      if (this._changed) {
474        // XXXmconley: At first, it seems strange to also persist the old way with
475        //             currentset - but this might actually be useful for switching
476        //             to old builds. We might want to keep this around for a little
477        //             bit.
478        this.persistCurrentSets();
479      }
480
481      // And drop all area references.
482      this.areas.clear();
483
484      // Let everybody in this window know that we're starting to
485      // exit customization mode.
486      CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
487
488      window.PanelUI.setMainView(window.PanelUI.mainView);
489      window.PanelUI.menuButton.disabled = false;
490
491      let customizeButton = document.getElementById("PanelUI-customize");
492      customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label"));
493      customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel"));
494      customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext"));
495      customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext"));
496
497      // We have to use setAttribute/removeAttribute here instead of the
498      // property because the XBL property will be set later, and right
499      // now we'd be setting an expando, which breaks the XBL property.
500      document.getElementById("PanelUI-help").removeAttribute("disabled");
501      document.getElementById("PanelUI-quit").removeAttribute("disabled");
502
503      panelContents.removeAttribute("customize-transitioning");
504
505      // We need to set this._customizing to false before removing the tab
506      // or the TabSelect event handler will think that we are exiting
507      // customization mode for a second time.
508      this._customizing = false;
509
510      let mainView = window.PanelUI.mainView;
511      if (this._mainViewContext) {
512        mainView.setAttribute("context", this._mainViewContext);
513      }
514
515      let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])");
516      for (let toolbar of customizableToolbars)
517        toolbar.removeAttribute("customizing");
518
519      this.window.PanelUI.endBatchUpdate();
520      delete this._lastLightweightTheme;
521      this._changed = false;
522      this._transitioning = false;
523      this._handler.isExitingCustomizeMode = false;
524      CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
525      CustomizableUI.notifyEndCustomizing(window);
526
527      if (this._wantToBeInCustomizeMode) {
528        this.enter();
529      }
530    }.bind(this)).then(null, function(e) {
531      log.error("Error exiting customize mode", e);
532      // We should ensure this has been called, and calling it again doesn't hurt:
533      window.PanelUI.endBatchUpdate();
534      this._handler.isExitingCustomizeMode = false;
535    }.bind(this));
536  },
537
538  /**
539   * The customize mode transition has 4 phases when entering:
540   * 1) Pre-customization mode
541   *    This is the starting phase of the browser.
542   * 2) LWT swapping
543   *    This is where we swap some of the lightweight theme styles in order
544   *    to make them work in customize mode. We set/unset a customization-
545   *    lwtheme attribute iff we're using a lightweight theme.
546   * 3) customize-entering
547   *    This phase is a transition, optimized for smoothness.
548   * 4) customize-entered
549   *    After the transition completes, this phase draws all of the
550   *    expensive detail that isn't necessary during the second phase.
551   *
552   * Exiting customization mode has a similar set of phases, but in reverse
553   * order - customize-entered, customize-exiting, remove LWT swapping,
554   * pre-customization mode.
555   *
556   * When in the customize-entering, customize-entered, or customize-exiting
557   * phases, there is a "customizing" attribute set on the main-window to simplify
558   * excluding certain styles while in any phase of customize mode.
559   */
560  _doTransition: function(aEntering) {
561    let deck = this.document.getElementById("content-deck");
562    let customizeTransitionEndPromise = new Promise(resolve => {
563      let customizeTransitionEnd = (aEvent) => {
564        if (aEvent != "timedout" &&
565            (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
566          return;
567        }
568        this.window.clearTimeout(catchAllTimeout);
569        // We request an animation frame to do the final stage of the transition
570        // to improve perceived performance. (bug 962677)
571        this.window.requestAnimationFrame(() => {
572          deck.removeEventListener("transitionend", customizeTransitionEnd);
573
574          if (!aEntering) {
575            this.document.documentElement.removeAttribute("customize-exiting");
576            this.document.documentElement.removeAttribute("customizing");
577          } else {
578            this.document.documentElement.setAttribute("customize-entered", true);
579            this.document.documentElement.removeAttribute("customize-entering");
580          }
581          CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
582
583          resolve();
584        });
585      };
586      deck.addEventListener("transitionend", customizeTransitionEnd);
587      let catchAll = () => customizeTransitionEnd("timedout");
588      let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
589    });
590
591    if (gDisableAnimation) {
592      this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true);
593    }
594
595    if (aEntering) {
596      this.document.documentElement.setAttribute("customizing", true);
597      this.document.documentElement.setAttribute("customize-entering", true);
598    } else {
599      this.document.documentElement.setAttribute("customize-exiting", true);
600      this.document.documentElement.removeAttribute("customize-entered");
601    }
602
603    return customizeTransitionEndPromise;
604  },
605
606  updateLWTStyling: function(aData) {
607    let docElement = this.document.documentElement;
608    if (!aData) {
609      let lwt = docElement._lightweightTheme;
610      aData = lwt.getData();
611    }
612    let headerURL = aData && aData.headerURL;
613    if (!headerURL) {
614      this.removeLWTStyling();
615      return;
616    }
617
618    let deck = this.document.getElementById("tab-view-deck");
619    let headerImageRef = this._getHeaderImageRef(aData);
620    docElement.setAttribute("customization-lwtheme", "true");
621
622    let toolboxRect = this.window.gNavToolbox.getBoundingClientRect();
623    let height = toolboxRect.bottom;
624
625    if (AppConstants.platform == "macosx") {
626      let drawingInTitlebar = !docElement.hasAttribute("drawtitle");
627      let titlebar = this.document.getElementById("titlebar");
628      if (drawingInTitlebar) {
629        titlebar.style.backgroundImage = headerImageRef;
630      } else {
631        titlebar.style.removeProperty("background-image");
632      }
633    }
634
635    let limitedBG = "-moz-image-rect(" + headerImageRef + ", 0, 100%, " +
636                    height + ", 0)";
637
638    let ridgeStart = height - 1;
639    let ridgeCenter = (ridgeStart + 1) + "px";
640    let ridgeEnd = (ridgeStart + 2) + "px";
641    ridgeStart = ridgeStart + "px";
642
643    let ridge = "linear-gradient(to bottom, " +
644                                 "transparent " + ridgeStart +
645                                 ", rgba(0,0,0,0.25) " + ridgeStart +
646                                 ", rgba(0,0,0,0.25) " + ridgeCenter +
647                                 ", rgba(255,255,255,0.5) " + ridgeCenter +
648                                 ", rgba(255,255,255,0.5) " + ridgeEnd + ", " +
649                                 "transparent " + ridgeEnd + ")";
650    deck.style.backgroundImage = ridge + ", " + limitedBG;
651
652    /* Remove the background styles from the <window> so we can style it instead. */
653    docElement.style.removeProperty("background-image");
654    docElement.style.removeProperty("background-color");
655  },
656
657  removeLWTStyling: function() {
658    let affectedNodes = AppConstants.platform == "macosx" ?
659                          ["tab-view-deck", "titlebar"] :
660                          ["tab-view-deck"];
661    for (let id of affectedNodes) {
662      let node = this.document.getElementById(id);
663      node.style.removeProperty("background-image");
664    }
665    let docElement = this.document.documentElement;
666    docElement.removeAttribute("customization-lwtheme");
667    let data = docElement._lightweightTheme.getData();
668    if (data && data.headerURL) {
669      docElement.style.backgroundImage = this._getHeaderImageRef(data);
670      docElement.style.backgroundColor = data.accentcolor || "white";
671    }
672  },
673
674  _getHeaderImageRef: function(aData) {
675    return "url(\"" + aData.headerURL.replace(/"/g, '\\"') + "\")";
676  },
677
678  maybeShowTip: function(aAnchor) {
679    let shown = false;
680    const kShownPref = "browser.customizemode.tip0.shown";
681    try {
682      shown = Services.prefs.getBoolPref(kShownPref);
683    } catch (ex) {}
684    if (shown)
685      return;
686
687    let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder");
688    let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage");
689    if (!messageNode.childElementCount) {
690      // Put the tip contents in the popup.
691      let bundle = this.document.getElementById("bundle_browser");
692      const kLabelClass = "customization-tipPanel-link";
693      messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [
694        "<label class=\"customization-tipPanel-em\" value=\"" +
695          bundle.getString("customizeTips.tip0.hint") + "\"/>",
696        this.document.getElementById("bundle_brand").getString("brandShortName"),
697        "<label class=\"" + kLabelClass + " text-link\" value=\"" +
698        bundle.getString("customizeTips.tip0.learnMore") + "\"/>"
699      ]);
700
701      messageNode.querySelector("." + kLabelClass).addEventListener("click", () => {
702        let url = Services.urlFormatter.formatURLPref("browser.customizemode.tip0.learnMoreUrl");
703        let browser = this.browser;
704        browser.selectedTab = browser.addTab(url);
705        this.hideTip();
706      });
707    }
708
709    this.tipPanel.hidden = false;
710    this.tipPanel.openPopup(anchorNode);
711    Services.prefs.setBoolPref(kShownPref, true);
712  },
713
714  hideTip: function() {
715    this.tipPanel.hidePopup();
716  },
717
718  _getCustomizableChildForNode: function(aNode) {
719    // NB: adjusted from _getCustomizableParent to keep that method fast
720    // (it's used during drags), and avoid multiple DOM loops
721    let areas = CustomizableUI.areas;
722    // Caching this length is important because otherwise we'll also iterate
723    // over items we add to the end from within the loop.
724    let numberOfAreas = areas.length;
725    for (let i = 0; i < numberOfAreas; i++) {
726      let area = areas[i];
727      let areaNode = aNode.ownerDocument.getElementById(area);
728      let customizationTarget = areaNode && areaNode.customizationTarget;
729      if (customizationTarget && customizationTarget != areaNode) {
730        areas.push(customizationTarget.id);
731      }
732      let overflowTarget = areaNode && areaNode.getAttribute("overflowtarget");
733      if (overflowTarget) {
734        areas.push(overflowTarget);
735      }
736    }
737    areas.push(kPaletteId);
738
739    while (aNode && aNode.parentNode) {
740      let parent = aNode.parentNode;
741      if (areas.indexOf(parent.id) != -1) {
742        return aNode;
743      }
744      aNode = parent;
745    }
746    return null;
747  },
748
749  addToToolbar: function(aNode) {
750    aNode = this._getCustomizableChildForNode(aNode);
751    if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
752      aNode = aNode.firstChild;
753    }
754    CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR);
755    if (!this._customizing) {
756      CustomizableUI.dispatchToolboxEvent("customizationchange");
757    }
758  },
759
760  addToPanel: function(aNode) {
761    aNode = this._getCustomizableChildForNode(aNode);
762    if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
763      aNode = aNode.firstChild;
764    }
765    CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_PANEL);
766    if (!this._customizing) {
767      CustomizableUI.dispatchToolboxEvent("customizationchange");
768    }
769  },
770
771  removeFromArea: function(aNode) {
772    aNode = this._getCustomizableChildForNode(aNode);
773    if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
774      aNode = aNode.firstChild;
775    }
776    CustomizableUI.removeWidgetFromArea(aNode.id);
777    if (!this._customizing) {
778      CustomizableUI.dispatchToolboxEvent("customizationchange");
779    }
780  },
781
782  populatePalette: function() {
783    let fragment = this.document.createDocumentFragment();
784    let toolboxPalette = this.window.gNavToolbox.palette;
785
786    try {
787      let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
788      for (let widget of unusedWidgets) {
789        let paletteItem = this.makePaletteItem(widget, "palette");
790        if (!paletteItem) {
791          continue;
792        }
793        fragment.appendChild(paletteItem);
794      }
795
796      this.visiblePalette.appendChild(fragment);
797      this._stowedPalette = this.window.gNavToolbox.palette;
798      this.window.gNavToolbox.palette = this.visiblePalette;
799    } catch (ex) {
800      log.error(ex);
801    }
802  },
803
804  // XXXunf Maybe this should use -moz-element instead of wrapping the node?
805  //       Would ensure no weird interactions/event handling from original node,
806  //       and makes it possible to put this in a lazy-loaded iframe/real tab
807  //       while still getting rid of the need for overlays.
808  makePaletteItem: function(aWidget, aPlace) {
809    let widgetNode = aWidget.forWindow(this.window).node;
810    if (!widgetNode) {
811      log.error("Widget with id " + aWidget.id + " does not return a valid node");
812      return null;
813    }
814    // Do not build a palette item for hidden widgets; there's not much to show.
815    if (widgetNode.hidden) {
816      return null;
817    }
818
819    let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
820    wrapper.appendChild(widgetNode);
821    return wrapper;
822  },
823
824  depopulatePalette: function() {
825    return Task.spawn(function*() {
826      this.visiblePalette.hidden = true;
827      let paletteChild = this.visiblePalette.firstChild;
828      let nextChild;
829      while (paletteChild) {
830        nextChild = paletteChild.nextElementSibling;
831        let provider = CustomizableUI.getWidget(paletteChild.id).provider;
832        if (provider == CustomizableUI.PROVIDER_XUL) {
833          let unwrappedPaletteItem =
834            yield this.deferredUnwrapToolbarItem(paletteChild);
835          this._stowedPalette.appendChild(unwrappedPaletteItem);
836        } else if (provider == CustomizableUI.PROVIDER_API) {
837          // XXXunf Currently this doesn't destroy the (now unused) node. It would
838          //       be good to do so, but we need to keep strong refs to it in
839          //       CustomizableUI (can't iterate of WeakMaps), and there's the
840          //       question of what behavior wrappers should have if consumers
841          //       keep hold of them.
842          // widget.destroyInstance(widgetNode);
843        } else if (provider == CustomizableUI.PROVIDER_SPECIAL) {
844          this.visiblePalette.removeChild(paletteChild);
845        }
846
847        paletteChild = nextChild;
848      }
849      this.visiblePalette.hidden = false;
850      this.window.gNavToolbox.palette = this._stowedPalette;
851    }.bind(this)).then(null, log.error);
852  },
853
854  isCustomizableItem: function(aNode) {
855    return aNode.localName == "toolbarbutton" ||
856           aNode.localName == "toolbaritem" ||
857           aNode.localName == "toolbarseparator" ||
858           aNode.localName == "toolbarspring" ||
859           aNode.localName == "toolbarspacer";
860  },
861
862  isWrappedToolbarItem: function(aNode) {
863    return aNode.localName == "toolbarpaletteitem";
864  },
865
866  deferredWrapToolbarItem: function(aNode, aPlace) {
867    return new Promise(resolve => {
868      dispatchFunction(() => {
869        let wrapper = this.wrapToolbarItem(aNode, aPlace);
870        resolve(wrapper);
871      });
872    });
873  },
874
875  wrapToolbarItem: function(aNode, aPlace) {
876    if (!this.isCustomizableItem(aNode)) {
877      return aNode;
878    }
879    let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
880
881    // It's possible that this toolbar node is "mid-flight" and doesn't have
882    // a parent, in which case we skip replacing it. This can happen if a
883    // toolbar item has been dragged into the palette. In that case, we tell
884    // CustomizableUI to remove the widget from its area before putting the
885    // widget in the palette - so the node will have no parent.
886    if (aNode.parentNode) {
887      aNode = aNode.parentNode.replaceChild(wrapper, aNode);
888    }
889    wrapper.appendChild(aNode);
890    return wrapper;
891  },
892
893  createOrUpdateWrapper: function(aNode, aPlace, aIsUpdate) {
894    let wrapper;
895    if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") {
896      wrapper = aNode.parentNode;
897      aPlace = wrapper.getAttribute("place");
898    } else {
899      wrapper = this.document.createElement("toolbarpaletteitem");
900      // "place" is used by toolkit to add the toolbarpaletteitem-palette
901      // binding to a toolbarpaletteitem, which gives it a label node for when
902      // it's sitting in the palette.
903      wrapper.setAttribute("place", aPlace);
904    }
905
906
907    // Ensure the wrapped item doesn't look like it's in any special state, and
908    // can't be interactved with when in the customization palette.
909    if (aNode.hasAttribute("command")) {
910      wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
911      aNode.removeAttribute("command");
912    }
913
914    if (aNode.hasAttribute("observes")) {
915      wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
916      aNode.removeAttribute("observes");
917    }
918
919    if (aNode.getAttribute("checked") == "true") {
920      wrapper.setAttribute("itemchecked", "true");
921      aNode.removeAttribute("checked");
922    }
923
924    if (aNode.hasAttribute("id")) {
925      wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
926    }
927
928    if (aNode.hasAttribute("label")) {
929      wrapper.setAttribute("title", aNode.getAttribute("label"));
930      wrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
931    } else if (aNode.hasAttribute("title")) {
932      wrapper.setAttribute("title", aNode.getAttribute("title"));
933      wrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
934    }
935
936    if (aNode.hasAttribute("flex")) {
937      wrapper.setAttribute("flex", aNode.getAttribute("flex"));
938    }
939
940    if (aPlace == "panel") {
941      if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
942        wrapper.setAttribute("haswideitem", "true");
943      } else if (wrapper.hasAttribute("haswideitem")) {
944        wrapper.removeAttribute("haswideitem");
945      }
946    }
947
948    let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
949    wrapper.setAttribute("removable", removable);
950
951    let contextMenuAttrName = "";
952    if (aNode.getAttribute("context")) {
953      contextMenuAttrName = "context";
954    } else if (aNode.getAttribute("contextmenu")) {
955      contextMenuAttrName = "contextmenu";
956    }
957    let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
958    let contextMenuForPlace = aPlace == "panel" ?
959                                kPanelItemContextMenu :
960                                kPaletteItemContextMenu;
961    if (aPlace != "toolbar") {
962      wrapper.setAttribute("context", contextMenuForPlace);
963    }
964    // Only keep track of the menu if it is non-default.
965    if (currentContextMenu &&
966        currentContextMenu != contextMenuForPlace) {
967      aNode.setAttribute("wrapped-context", currentContextMenu);
968      aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName)
969      aNode.removeAttribute(contextMenuAttrName);
970    } else if (currentContextMenu == contextMenuForPlace) {
971      aNode.removeAttribute(contextMenuAttrName);
972    }
973
974    // Only add listeners for newly created wrappers:
975    if (!aIsUpdate) {
976      wrapper.addEventListener("mousedown", this);
977      wrapper.addEventListener("mouseup", this);
978    }
979
980    return wrapper;
981  },
982
983  deferredUnwrapToolbarItem: function(aWrapper) {
984    return new Promise(resolve => {
985      dispatchFunction(() => {
986        let item = null;
987        try {
988          item = this.unwrapToolbarItem(aWrapper);
989        } catch (ex) {
990          Cu.reportError(ex);
991        }
992        resolve(item);
993      });
994    });
995  },
996
997  unwrapToolbarItem: function(aWrapper) {
998    if (aWrapper.nodeName != "toolbarpaletteitem") {
999      return aWrapper;
1000    }
1001    aWrapper.removeEventListener("mousedown", this);
1002    aWrapper.removeEventListener("mouseup", this);
1003
1004    let place = aWrapper.getAttribute("place");
1005
1006    let toolbarItem = aWrapper.firstChild;
1007    if (!toolbarItem) {
1008      log.error("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id);
1009      aWrapper.remove();
1010      return null;
1011    }
1012
1013    if (aWrapper.hasAttribute("itemobserves")) {
1014      toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves"));
1015    }
1016
1017    if (aWrapper.hasAttribute("itemchecked")) {
1018      toolbarItem.checked = true;
1019    }
1020
1021    if (aWrapper.hasAttribute("itemcommand")) {
1022      let commandID = aWrapper.getAttribute("itemcommand");
1023      toolbarItem.setAttribute("command", commandID);
1024
1025      // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
1026      let command = this.document.getElementById(commandID);
1027      if (command && command.hasAttribute("disabled")) {
1028        toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
1029      }
1030    }
1031
1032    let wrappedContext = toolbarItem.getAttribute("wrapped-context");
1033    if (wrappedContext) {
1034      let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
1035      toolbarItem.setAttribute(contextAttrName, wrappedContext);
1036      toolbarItem.removeAttribute("wrapped-contextAttrName");
1037      toolbarItem.removeAttribute("wrapped-context");
1038    } else if (place == "panel") {
1039      toolbarItem.setAttribute("context", kPanelItemContextMenu);
1040    }
1041
1042    if (aWrapper.parentNode) {
1043      aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
1044    }
1045    return toolbarItem;
1046  },
1047
1048  _wrapToolbarItem: function*(aArea) {
1049    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1050    if (!target || this.areas.has(target)) {
1051      return null;
1052    }
1053
1054    this._addDragHandlers(target);
1055    for (let child of target.children) {
1056      if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
1057        yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)).then(null, log.error);
1058      }
1059    }
1060    this.areas.add(target);
1061    return target;
1062  },
1063
1064  _wrapToolbarItemSync: function(aArea) {
1065    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1066    if (!target || this.areas.has(target)) {
1067      return null;
1068    }
1069
1070    this._addDragHandlers(target);
1071    try {
1072      for (let child of target.children) {
1073        if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
1074          this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1075        }
1076      }
1077    } catch (ex) {
1078      log.error(ex, ex.stack);
1079    }
1080
1081    this.areas.add(target);
1082    return target;
1083  },
1084
1085  _wrapToolbarItems: function*() {
1086    for (let area of CustomizableUI.areas) {
1087      yield this._wrapToolbarItem(area);
1088    }
1089  },
1090
1091  _addDragHandlers: function(aTarget) {
1092    aTarget.addEventListener("dragstart", this, true);
1093    aTarget.addEventListener("dragover", this, true);
1094    aTarget.addEventListener("dragexit", this, true);
1095    aTarget.addEventListener("drop", this, true);
1096    aTarget.addEventListener("dragend", this, true);
1097  },
1098
1099  _wrapItemsInArea: function(target) {
1100    for (let child of target.children) {
1101      if (this.isCustomizableItem(child)) {
1102        this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1103      }
1104    }
1105  },
1106
1107  _removeDragHandlers: function(aTarget) {
1108    aTarget.removeEventListener("dragstart", this, true);
1109    aTarget.removeEventListener("dragover", this, true);
1110    aTarget.removeEventListener("dragexit", this, true);
1111    aTarget.removeEventListener("drop", this, true);
1112    aTarget.removeEventListener("dragend", this, true);
1113  },
1114
1115  _unwrapItemsInArea: function(target) {
1116    for (let toolbarItem of target.children) {
1117      if (this.isWrappedToolbarItem(toolbarItem)) {
1118        this.unwrapToolbarItem(toolbarItem);
1119      }
1120    }
1121  },
1122
1123  _unwrapToolbarItems: function() {
1124    return Task.spawn(function*() {
1125      for (let target of this.areas) {
1126        for (let toolbarItem of target.children) {
1127          if (this.isWrappedToolbarItem(toolbarItem)) {
1128            yield this.deferredUnwrapToolbarItem(toolbarItem);
1129          }
1130        }
1131        this._removeDragHandlers(target);
1132      }
1133      this.areas.clear();
1134    }.bind(this)).then(null, log.error);
1135  },
1136
1137  _removeExtraToolbarsIfEmpty: function() {
1138    let toolbox = this.window.gNavToolbox;
1139    for (let child of toolbox.children) {
1140      if (child.hasAttribute("customindex")) {
1141        let placements = CustomizableUI.getWidgetIdsInArea(child.id);
1142        if (!placements.length) {
1143          CustomizableUI.removeExtraToolbar(child.id);
1144        }
1145      }
1146    }
1147  },
1148
1149  persistCurrentSets: function(aSetBeforePersisting)  {
1150    let document = this.document;
1151    let toolbars = document.querySelectorAll("toolbar[customizable='true'][currentset]");
1152    for (let toolbar of toolbars) {
1153      if (aSetBeforePersisting) {
1154        let set = toolbar.currentSet;
1155        toolbar.setAttribute("currentset", set);
1156      }
1157      // Persist the currentset attribute directly on hardcoded toolbars.
1158      document.persist(toolbar.id, "currentset");
1159    }
1160  },
1161
1162  reset: function() {
1163    this.resetting = true;
1164    // Disable the reset button temporarily while resetting:
1165    let btn = this.document.getElementById("customization-reset-button");
1166    BrowserUITelemetry.countCustomizationEvent("reset");
1167    btn.disabled = true;
1168    return Task.spawn(function*() {
1169      this._removePanelCustomizationPlaceholders();
1170      yield this.depopulatePalette();
1171      yield this._unwrapToolbarItems();
1172
1173      CustomizableUI.reset();
1174
1175      this._updateLWThemeButtonIcon();
1176
1177      yield this._wrapToolbarItems();
1178      this.populatePalette();
1179
1180      this.persistCurrentSets(true);
1181
1182      this._updateResetButton();
1183      this._updateUndoResetButton();
1184      this._updateEmptyPaletteNotice();
1185      this._showPanelCustomizationPlaceholders();
1186      this.resetting = false;
1187      if (!this._wantToBeInCustomizeMode) {
1188        this.exit();
1189      }
1190    }.bind(this)).then(null, log.error);
1191  },
1192
1193  undoReset: function() {
1194    this.resetting = true;
1195
1196    return Task.spawn(function*() {
1197      this._removePanelCustomizationPlaceholders();
1198      yield this.depopulatePalette();
1199      yield this._unwrapToolbarItems();
1200
1201      CustomizableUI.undoReset();
1202
1203      this._updateLWThemeButtonIcon();
1204
1205      yield this._wrapToolbarItems();
1206      this.populatePalette();
1207
1208      this.persistCurrentSets(true);
1209
1210      this._updateResetButton();
1211      this._updateUndoResetButton();
1212      this._updateEmptyPaletteNotice();
1213      this.resetting = false;
1214    }.bind(this)).then(null, log.error);
1215  },
1216
1217  _onToolbarVisibilityChange: function(aEvent) {
1218    let toolbar = aEvent.target;
1219    if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
1220      toolbar.setAttribute("customizing", "true");
1221    } else {
1222      toolbar.removeAttribute("customizing");
1223    }
1224    this._onUIChange();
1225    this.updateLWTStyling();
1226  },
1227
1228  onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
1229    this._onUIChange();
1230  },
1231
1232  onWidgetAdded: function(aWidgetId, aArea, aPosition) {
1233    this._onUIChange();
1234  },
1235
1236  onWidgetRemoved: function(aWidgetId, aArea) {
1237    this._onUIChange();
1238  },
1239
1240  onWidgetBeforeDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
1241    if (aContainer.ownerGlobal != this.window || this.resetting) {
1242      return;
1243    }
1244    if (aContainer.id == CustomizableUI.AREA_PANEL) {
1245      this._removePanelCustomizationPlaceholders();
1246    }
1247    // If we get called for widgets that aren't in the window yet, they might not have
1248    // a parentNode at all.
1249    if (aNodeToChange.parentNode) {
1250      this.unwrapToolbarItem(aNodeToChange.parentNode);
1251    }
1252    if (aSecondaryNode) {
1253      this.unwrapToolbarItem(aSecondaryNode.parentNode);
1254    }
1255  },
1256
1257  onWidgetAfterDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
1258    if (aContainer.ownerGlobal != this.window || this.resetting) {
1259      return;
1260    }
1261    // If the node is still attached to the container, wrap it again:
1262    if (aNodeToChange.parentNode) {
1263      let place = CustomizableUI.getPlaceForItem(aNodeToChange);
1264      this.wrapToolbarItem(aNodeToChange, place);
1265      if (aSecondaryNode) {
1266        this.wrapToolbarItem(aSecondaryNode, place);
1267      }
1268    } else {
1269      // If not, it got removed.
1270
1271      // If an API-based widget is removed while customizing, append it to the palette.
1272      // The _applyDrop code itself will take care of positioning it correctly, if
1273      // applicable. We need the code to be here so removing widgets using CustomizableUI's
1274      // API also does the right thing (and adds it to the palette)
1275      let widgetId = aNodeToChange.id;
1276      let widget = CustomizableUI.getWidget(widgetId);
1277      if (widget.provider == CustomizableUI.PROVIDER_API) {
1278        let paletteItem = this.makePaletteItem(widget, "palette");
1279        this.visiblePalette.appendChild(paletteItem);
1280      }
1281    }
1282    if (aContainer.id == CustomizableUI.AREA_PANEL) {
1283      this._showPanelCustomizationPlaceholders();
1284    }
1285  },
1286
1287  onWidgetDestroyed: function(aWidgetId) {
1288    let wrapper = this.document.getElementById("wrapper-" + aWidgetId);
1289    if (wrapper) {
1290      let wasInPanel = wrapper.parentNode == this.panelUIContents;
1291      wrapper.remove();
1292      if (wasInPanel) {
1293        this._showPanelCustomizationPlaceholders();
1294      }
1295    }
1296  },
1297
1298  onWidgetAfterCreation: function(aWidgetId, aArea) {
1299    // If the node was added to an area, we would have gotten an onWidgetAdded notification,
1300    // plus associated DOM change notifications, so only do stuff for the palette:
1301    if (!aArea) {
1302      let widgetNode = this.document.getElementById(aWidgetId);
1303      if (widgetNode) {
1304        this.wrapToolbarItem(widgetNode, "palette");
1305      } else {
1306        let widget = CustomizableUI.getWidget(aWidgetId);
1307        this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
1308      }
1309    }
1310  },
1311
1312  onAreaNodeRegistered: function(aArea, aContainer) {
1313    if (aContainer.ownerDocument == this.document) {
1314      this._wrapItemsInArea(aContainer);
1315      this._addDragHandlers(aContainer);
1316      DragPositionManager.add(this.window, aArea, aContainer);
1317      this.areas.add(aContainer);
1318    }
1319  },
1320
1321  onAreaNodeUnregistered: function(aArea, aContainer, aReason) {
1322    if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) {
1323      this._unwrapItemsInArea(aContainer);
1324      this._removeDragHandlers(aContainer);
1325      DragPositionManager.remove(this.window, aArea, aContainer);
1326      this.areas.delete(aContainer);
1327    }
1328  },
1329
1330  openAddonsManagerThemes: function(aEvent) {
1331    aEvent.target.parentNode.parentNode.hidePopup();
1332    this.window.BrowserOpenAddonsMgr('addons://list/theme');
1333  },
1334
1335  getMoreThemes: function(aEvent) {
1336    aEvent.target.parentNode.parentNode.hidePopup();
1337    let getMoreURL = Services.urlFormatter.formatURLPref("lightweightThemes.getMoreURL");
1338    this.window.openUILinkIn(getMoreURL, "tab");
1339  },
1340
1341  onLWThemesMenuShowing: function(aEvent) {
1342    const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}";
1343    const RECENT_LWT_COUNT = 5;
1344
1345    this._clearLWThemesMenu(aEvent.target);
1346
1347    function previewTheme(aEvent) {
1348      LightweightThemeManager.previewTheme(aEvent.target.theme.id != DEFAULT_THEME_ID ?
1349                                           aEvent.target.theme : null);
1350    }
1351
1352    function resetPreview() {
1353      LightweightThemeManager.resetPreview();
1354    }
1355
1356    let onThemeSelected = panel => {
1357      this._updateLWThemeButtonIcon();
1358      this._onUIChange();
1359      panel.hidePopup();
1360    };
1361
1362    AddonManager.getAddonByID(DEFAULT_THEME_ID, function(aDefaultTheme) {
1363      let doc = this.window.document;
1364
1365      function buildToolbarButton(aTheme) {
1366        let tbb = doc.createElement("toolbarbutton");
1367        tbb.theme = aTheme;
1368        tbb.setAttribute("label", aTheme.name);
1369        if (aDefaultTheme == aTheme) {
1370          // The actual icon is set up so it looks nice in about:addons, but
1371          // we'd like the version that's correct for the OS we're on, so we set
1372          // an attribute that our styling will then use to display the icon.
1373          tbb.setAttribute("defaulttheme", "true");
1374        } else {
1375          tbb.setAttribute("image", aTheme.iconURL);
1376        }
1377        if (aTheme.description)
1378          tbb.setAttribute("tooltiptext", aTheme.description);
1379        tbb.setAttribute("tabindex", "0");
1380        tbb.classList.add("customization-lwtheme-menu-theme");
1381        tbb.setAttribute("aria-checked", aTheme.isActive);
1382        tbb.setAttribute("role", "menuitemradio");
1383        if (aTheme.isActive) {
1384          tbb.setAttribute("active", "true");
1385        }
1386        tbb.addEventListener("focus", previewTheme);
1387        tbb.addEventListener("mouseover", previewTheme);
1388        tbb.addEventListener("blur", resetPreview);
1389        tbb.addEventListener("mouseout", resetPreview);
1390
1391        return tbb;
1392      }
1393
1394      let themes = [aDefaultTheme];
1395      let lwts = LightweightThemeManager.usedThemes;
1396      if (lwts.length > RECENT_LWT_COUNT)
1397        lwts.length = RECENT_LWT_COUNT;
1398      let currentLwt = LightweightThemeManager.currentTheme;
1399      for (let lwt of lwts) {
1400        lwt.isActive = !!currentLwt && (lwt.id == currentLwt.id);
1401        themes.push(lwt);
1402      }
1403
1404      let footer = doc.getElementById("customization-lwtheme-menu-footer");
1405      let panel = footer.parentNode;
1406      let recommendedLabel = doc.getElementById("customization-lwtheme-menu-recommended");
1407      for (let theme of themes) {
1408        let button = buildToolbarButton(theme);
1409        button.addEventListener("command", () => {
1410          if ("userDisabled" in button.theme)
1411            button.theme.userDisabled = false;
1412          else
1413            LightweightThemeManager.currentTheme = button.theme;
1414          onThemeSelected(panel);
1415        });
1416        panel.insertBefore(button, recommendedLabel);
1417      }
1418
1419      let lwthemePrefs = Services.prefs.getBranch("lightweightThemes.");
1420      let recommendedThemes = lwthemePrefs.getComplexValue("recommendedThemes",
1421                                                           Ci.nsISupportsString).data;
1422      recommendedThemes = JSON.parse(recommendedThemes);
1423      let sb = Services.strings.createBundle("chrome://browser/locale/lightweightThemes.properties");
1424      for (let theme of recommendedThemes) {
1425        theme.name = sb.GetStringFromName("lightweightThemes." + theme.id + ".name");
1426        theme.description = sb.GetStringFromName("lightweightThemes." + theme.id + ".description");
1427        let button = buildToolbarButton(theme);
1428        button.addEventListener("command", () => {
1429          LightweightThemeManager.setLocalTheme(button.theme);
1430          recommendedThemes = recommendedThemes.filter((aTheme) => { return aTheme.id != button.theme.id; });
1431          let string = Cc["@mozilla.org/supports-string;1"]
1432                         .createInstance(Ci.nsISupportsString);
1433          string.data = JSON.stringify(recommendedThemes);
1434          lwthemePrefs.setComplexValue("recommendedThemes",
1435                                       Ci.nsISupportsString, string);
1436          onThemeSelected(panel);
1437        });
1438        panel.insertBefore(button, footer);
1439      }
1440      let hideRecommendedLabel = (footer.previousSibling == recommendedLabel);
1441      recommendedLabel.hidden = hideRecommendedLabel;
1442    }.bind(this));
1443  },
1444
1445  _clearLWThemesMenu: function(panel) {
1446    let footer = this.document.getElementById("customization-lwtheme-menu-footer");
1447    let recommendedLabel = this.document.getElementById("customization-lwtheme-menu-recommended");
1448    for (let element of [footer, recommendedLabel]) {
1449      while (element.previousSibling &&
1450             element.previousSibling.localName == "toolbarbutton") {
1451        element.previousSibling.remove();
1452      }
1453    }
1454
1455    // Workaround for bug 1059934
1456    panel.removeAttribute("height");
1457  },
1458
1459  _onUIChange: function() {
1460    this._changed = true;
1461    if (!this.resetting) {
1462      this._updateResetButton();
1463      this._updateUndoResetButton();
1464      this._updateEmptyPaletteNotice();
1465    }
1466    CustomizableUI.dispatchToolboxEvent("customizationchange");
1467  },
1468
1469  _updateEmptyPaletteNotice: function() {
1470    let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
1471    this.paletteEmptyNotice.hidden = !!paletteItems.length;
1472  },
1473
1474  _updateResetButton: function() {
1475    let btn = this.document.getElementById("customization-reset-button");
1476    btn.disabled = CustomizableUI.inDefaultState;
1477  },
1478
1479  _updateUndoResetButton: function() {
1480    let undoResetButton =  this.document.getElementById("customization-undo-reset-button");
1481    undoResetButton.hidden = !CustomizableUI.canUndoReset;
1482  },
1483
1484  handleEvent: function(aEvent) {
1485    switch (aEvent.type) {
1486      case "toolbarvisibilitychange":
1487        this._onToolbarVisibilityChange(aEvent);
1488        break;
1489      case "dragstart":
1490        this._onDragStart(aEvent);
1491        break;
1492      case "dragover":
1493        this._onDragOver(aEvent);
1494        break;
1495      case "drop":
1496        this._onDragDrop(aEvent);
1497        break;
1498      case "dragexit":
1499        this._onDragExit(aEvent);
1500        break;
1501      case "dragend":
1502        this._onDragEnd(aEvent);
1503        break;
1504      case "command":
1505        if (aEvent.originalTarget == this.window.PanelUI.menuButton) {
1506          this.exit();
1507          aEvent.preventDefault();
1508        }
1509        break;
1510      case "mousedown":
1511        this._onMouseDown(aEvent);
1512        break;
1513      case "mouseup":
1514        this._onMouseUp(aEvent);
1515        break;
1516      case "keypress":
1517        if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
1518          this.exit();
1519        }
1520        break;
1521      case "unload":
1522        this.uninit();
1523        break;
1524    }
1525  },
1526
1527  observe: function(aSubject, aTopic, aData) {
1528    switch (aTopic) {
1529      case "nsPref:changed":
1530        this._updateResetButton();
1531        this._updateUndoResetButton();
1532        if (AppConstants.CAN_DRAW_IN_TITLEBAR) {
1533          this._updateTitlebarButton();
1534        }
1535        break;
1536      case "lightweight-theme-window-updated":
1537        if (aSubject == this.window) {
1538          aData = JSON.parse(aData);
1539          if (!aData) {
1540            this.removeLWTStyling();
1541          } else {
1542            this.updateLWTStyling(aData);
1543          }
1544        }
1545        break;
1546    }
1547  },
1548
1549  _updateTitlebarButton: function() {
1550    if (!AppConstants.CAN_DRAW_IN_TITLEBAR) {
1551      return;
1552    }
1553    let drawInTitlebar = true;
1554    try {
1555      drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref);
1556    } catch (ex) { }
1557    let button = this.document.getElementById("customization-titlebar-visibility-button");
1558    // Drawing in the titlebar means 'hiding' the titlebar:
1559    if (drawInTitlebar) {
1560      button.removeAttribute("checked");
1561    } else {
1562      button.setAttribute("checked", "true");
1563    }
1564  },
1565
1566  toggleTitlebar: function(aShouldShowTitlebar) {
1567    if (!AppConstants.CAN_DRAW_IN_TITLEBAR) {
1568      return;
1569    }
1570    // Drawing in the titlebar means not showing the titlebar, hence the negation:
1571    Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
1572  },
1573
1574  _onDragStart: function(aEvent) {
1575    __dumpDragData(aEvent);
1576    let item = aEvent.target;
1577    while (item && item.localName != "toolbarpaletteitem") {
1578      if (item.localName == "toolbar") {
1579        return;
1580      }
1581      item = item.parentNode;
1582    }
1583
1584    let draggedItem = item.firstChild;
1585    let placeForItem = CustomizableUI.getPlaceForItem(item);
1586    let isRemovable = placeForItem == "palette" ||
1587                      CustomizableUI.isWidgetRemovable(draggedItem);
1588    if (item.classList.contains(kPlaceholderClass) || !isRemovable) {
1589      return;
1590    }
1591
1592    let dt = aEvent.dataTransfer;
1593    let documentId = aEvent.target.ownerDocument.documentElement.id;
1594    let isInToolbar = placeForItem == "toolbar";
1595
1596    dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
1597    dt.effectAllowed = "move";
1598
1599    let itemRect = draggedItem.getBoundingClientRect();
1600    let itemCenter = {x: itemRect.left + itemRect.width / 2,
1601                      y: itemRect.top + itemRect.height / 2};
1602    this._dragOffset = {x: aEvent.clientX - itemCenter.x,
1603                        y: aEvent.clientY - itemCenter.y};
1604
1605    gDraggingInToolbars = new Set();
1606
1607    // Hack needed so that the dragimage will still show the
1608    // item as it appeared before it was hidden.
1609    this._initializeDragAfterMove = function() {
1610      // For automated tests, we sometimes start exiting customization mode
1611      // before this fires, which leaves us with placeholders inserted after
1612      // we've exited. So we need to check that we are indeed customizing.
1613      if (this._customizing && !this._transitioning) {
1614        item.hidden = true;
1615        this._showPanelCustomizationPlaceholders();
1616        DragPositionManager.start(this.window);
1617        if (item.nextSibling) {
1618          this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar);
1619          this._dragOverItem = item.nextSibling;
1620        } else if (isInToolbar && item.previousSibling) {
1621          this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar);
1622          this._dragOverItem = item.previousSibling;
1623        }
1624      }
1625      this._initializeDragAfterMove = null;
1626      this.window.clearTimeout(this._dragInitializeTimeout);
1627    }.bind(this);
1628    this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
1629  },
1630
1631  _onDragOver: function(aEvent) {
1632    if (this._isUnwantedDragDrop(aEvent)) {
1633      return;
1634    }
1635    if (this._initializeDragAfterMove) {
1636      this._initializeDragAfterMove();
1637    }
1638
1639    __dumpDragData(aEvent);
1640
1641    let document = aEvent.target.ownerDocument;
1642    let documentId = document.documentElement.id;
1643    if (!aEvent.dataTransfer.mozTypesAt(0)) {
1644      return;
1645    }
1646
1647    let draggedItemId =
1648      aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
1649    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1650    let targetArea = this._getCustomizableParent(aEvent.currentTarget);
1651    let originArea = this._getCustomizableParent(draggedWrapper);
1652
1653    // Do nothing if the target or origin are not customizable.
1654    if (!targetArea || !originArea) {
1655      return;
1656    }
1657
1658    // Do nothing if the widget is not allowed to be removed.
1659    if (targetArea.id == kPaletteId &&
1660       !CustomizableUI.isWidgetRemovable(draggedItemId)) {
1661      return;
1662    }
1663
1664    // Do nothing if the widget is not allowed to move to the target area.
1665    if (targetArea.id != kPaletteId &&
1666        !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
1667      return;
1668    }
1669
1670    let targetIsToolbar = CustomizableUI.getAreaType(targetArea.id) == "toolbar";
1671    let targetNode = this._getDragOverNode(aEvent, targetArea, targetIsToolbar, draggedItemId);
1672
1673    // We need to determine the place that the widget is being dropped in
1674    // the target.
1675    let dragOverItem, dragValue;
1676    if (targetNode == targetArea.customizationTarget) {
1677      // We'll assume if the user is dragging directly over the target, that
1678      // they're attempting to append a child to that target.
1679      dragOverItem = (targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
1680                                        targetNode.lastChild) || targetNode;
1681      dragValue = "after";
1682    } else {
1683      let targetParent = targetNode.parentNode;
1684      let position = Array.indexOf(targetParent.children, targetNode);
1685      if (position == -1) {
1686        dragOverItem = targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
1687                                         targetParent.lastChild;
1688        dragValue = "after";
1689      } else {
1690        dragOverItem = targetParent.children[position];
1691        if (!targetIsToolbar) {
1692          dragValue = "before";
1693        } else {
1694          // Check if the aDraggedItem is hovered past the first half of dragOverItem
1695          let window = dragOverItem.ownerGlobal;
1696          let direction = window.getComputedStyle(dragOverItem, null).direction;
1697          let itemRect = dragOverItem.getBoundingClientRect();
1698          let dropTargetCenter = itemRect.left + (itemRect.width / 2);
1699          let existingDir = dragOverItem.getAttribute("dragover");
1700          if ((existingDir == "before") == (direction == "ltr")) {
1701            dropTargetCenter += (parseInt(dragOverItem.style.borderLeftWidth) || 0) / 2;
1702          } else {
1703            dropTargetCenter -= (parseInt(dragOverItem.style.borderRightWidth) || 0) / 2;
1704          }
1705          let before = direction == "ltr" ? aEvent.clientX < dropTargetCenter : aEvent.clientX > dropTargetCenter;
1706          dragValue = before ? "before" : "after";
1707        }
1708      }
1709    }
1710
1711    if (this._dragOverItem && dragOverItem != this._dragOverItem) {
1712      this._cancelDragActive(this._dragOverItem, dragOverItem);
1713    }
1714
1715    if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) {
1716      if (dragOverItem != targetArea.customizationTarget) {
1717        this._setDragActive(dragOverItem, dragValue, draggedItemId, targetIsToolbar);
1718      } else if (targetIsToolbar) {
1719        this._updateToolbarCustomizationOutline(this.window, targetArea);
1720      }
1721      this._dragOverItem = dragOverItem;
1722    }
1723
1724    aEvent.preventDefault();
1725    aEvent.stopPropagation();
1726  },
1727
1728  _onDragDrop: function(aEvent) {
1729    if (this._isUnwantedDragDrop(aEvent)) {
1730      return;
1731    }
1732
1733    __dumpDragData(aEvent);
1734    this._initializeDragAfterMove = null;
1735    this.window.clearTimeout(this._dragInitializeTimeout);
1736
1737    let targetArea = this._getCustomizableParent(aEvent.currentTarget);
1738    let document = aEvent.target.ownerDocument;
1739    let documentId = document.documentElement.id;
1740    let draggedItemId =
1741      aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
1742    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1743    let originArea = this._getCustomizableParent(draggedWrapper);
1744    if (this._dragSizeMap) {
1745      this._dragSizeMap = new WeakMap();
1746    }
1747    // Do nothing if the target area or origin area are not customizable.
1748    if (!targetArea || !originArea) {
1749      return;
1750    }
1751    let targetNode = this._dragOverItem;
1752    let dropDir = targetNode.getAttribute("dragover");
1753    // Need to insert *after* this node if we promised the user that:
1754    if (targetNode != targetArea && dropDir == "after") {
1755      if (targetNode.nextSibling) {
1756        targetNode = targetNode.nextSibling;
1757      } else {
1758        targetNode = targetArea;
1759      }
1760    }
1761    // If the target node is a placeholder, get its sibling as the real target.
1762    while (targetNode.classList.contains(kPlaceholderClass) && targetNode.nextSibling) {
1763      targetNode = targetNode.nextSibling;
1764    }
1765    if (targetNode.tagName == "toolbarpaletteitem") {
1766      targetNode = targetNode.firstChild;
1767    }
1768
1769    this._cancelDragActive(this._dragOverItem, null, true);
1770    this._removePanelCustomizationPlaceholders();
1771
1772    try {
1773      this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode);
1774    } catch (ex) {
1775      log.error(ex, ex.stack);
1776    }
1777
1778    this._showPanelCustomizationPlaceholders();
1779  },
1780
1781  _applyDrop: function(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
1782    let document = aEvent.target.ownerDocument;
1783    let draggedItem = document.getElementById(aDraggedItemId);
1784    draggedItem.hidden = false;
1785    draggedItem.removeAttribute("mousedown");
1786
1787    // Do nothing if the target was dropped onto itself (ie, no change in area
1788    // or position).
1789    if (draggedItem == aTargetNode) {
1790      return;
1791    }
1792
1793    // Is the target area the customization palette?
1794    if (aTargetArea.id == kPaletteId) {
1795      // Did we drag from outside the palette?
1796      if (aOriginArea.id !== kPaletteId) {
1797        if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) {
1798          return;
1799        }
1800
1801        CustomizableUI.removeWidgetFromArea(aDraggedItemId);
1802        BrowserUITelemetry.countCustomizationEvent("remove");
1803        // Special widgets are removed outright, we can return here:
1804        if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
1805          return;
1806        }
1807      }
1808      draggedItem = draggedItem.parentNode;
1809
1810      // If the target node is the palette itself, just append
1811      if (aTargetNode == this.visiblePalette) {
1812        this.visiblePalette.appendChild(draggedItem);
1813      } else {
1814        // The items in the palette are wrapped, so we need the target node's parent here:
1815        this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
1816      }
1817      if (aOriginArea.id !== kPaletteId) {
1818        // The dragend event already fires when the item moves within the palette.
1819        this._onDragEnd(aEvent);
1820      }
1821      return;
1822    }
1823
1824    if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
1825      return;
1826    }
1827
1828    // Skipintoolbarset items won't really be moved:
1829    if (draggedItem.getAttribute("skipintoolbarset") == "true") {
1830      // These items should never leave their area:
1831      if (aTargetArea != aOriginArea) {
1832        return;
1833      }
1834      let place = draggedItem.parentNode.getAttribute("place");
1835      this.unwrapToolbarItem(draggedItem.parentNode);
1836      if (aTargetNode == aTargetArea.customizationTarget) {
1837        aTargetArea.customizationTarget.appendChild(draggedItem);
1838      } else {
1839        this.unwrapToolbarItem(aTargetNode.parentNode);
1840        aTargetArea.customizationTarget.insertBefore(draggedItem, aTargetNode);
1841        this.wrapToolbarItem(aTargetNode, place);
1842      }
1843      this.wrapToolbarItem(draggedItem, place);
1844      BrowserUITelemetry.countCustomizationEvent("move");
1845      return;
1846    }
1847
1848    // Is the target the customization area itself? If so, we just add the
1849    // widget to the end of the area.
1850    if (aTargetNode == aTargetArea.customizationTarget) {
1851      CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
1852      // For the purposes of BrowserUITelemetry, we consider both moving a widget
1853      // within the same area, and adding a widget from one area to another area
1854      // as a "move". An "add" is only when we move an item from the palette into
1855      // an area.
1856      let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
1857      BrowserUITelemetry.countCustomizationEvent(custEventType);
1858      this._onDragEnd(aEvent);
1859      return;
1860    }
1861
1862    // We need to determine the place that the widget is being dropped in
1863    // the target.
1864    let placement;
1865    let itemForPlacement = aTargetNode;
1866    // Skip the skipintoolbarset items when determining the place of the item:
1867    while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
1868           itemForPlacement.parentNode &&
1869           itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") {
1870      itemForPlacement = itemForPlacement.parentNode.nextSibling;
1871      if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") {
1872        itemForPlacement = itemForPlacement.firstChild;
1873      }
1874    }
1875    if (itemForPlacement && !itemForPlacement.classList.contains(kPlaceholderClass)) {
1876      let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ?
1877                            itemForPlacement.firstChild && itemForPlacement.firstChild.id :
1878                            itemForPlacement.id;
1879      placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
1880    }
1881    if (!placement) {
1882      log.debug("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className);
1883    }
1884    let position = placement ? placement.position : null;
1885
1886    // Is the target area the same as the origin? Since we've already handled
1887    // the possibility that the target is the customization palette, we know
1888    // that the widget is moving within a customizable area.
1889    if (aTargetArea == aOriginArea) {
1890      CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position);
1891    } else {
1892      CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
1893    }
1894
1895    this._onDragEnd(aEvent);
1896
1897    // For BrowserUITelemetry, an "add" is only when we move an item from the palette
1898    // into an area. Otherwise, it's a move.
1899    let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
1900    BrowserUITelemetry.countCustomizationEvent(custEventType);
1901
1902    // If we dropped onto a skipintoolbarset item, manually correct the drop location:
1903    if (aTargetNode != itemForPlacement) {
1904      let draggedWrapper = draggedItem.parentNode;
1905      let container = draggedWrapper.parentNode;
1906      container.insertBefore(draggedWrapper, aTargetNode.parentNode);
1907    }
1908  },
1909
1910  _onDragExit: function(aEvent) {
1911    if (this._isUnwantedDragDrop(aEvent)) {
1912      return;
1913    }
1914
1915    __dumpDragData(aEvent);
1916
1917    // When leaving customization areas, cancel the drag on the last dragover item
1918    // We've attached the listener to areas, so aEvent.currentTarget will be the area.
1919    // We don't care about dragexit events fired on descendants of the area,
1920    // so we check that the event's target is the same as the area to which the listener
1921    // was attached.
1922    if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
1923      this._cancelDragActive(this._dragOverItem);
1924      this._dragOverItem = null;
1925    }
1926  },
1927
1928  /**
1929   * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
1930   */
1931  _onDragEnd: function(aEvent) {
1932    if (this._isUnwantedDragDrop(aEvent)) {
1933      return;
1934    }
1935    this._initializeDragAfterMove = null;
1936    this.window.clearTimeout(this._dragInitializeTimeout);
1937    __dumpDragData(aEvent, "_onDragEnd");
1938
1939    let document = aEvent.target.ownerDocument;
1940    document.documentElement.removeAttribute("customizing-movingItem");
1941
1942    let documentId = document.documentElement.id;
1943    if (!aEvent.dataTransfer.mozTypesAt(0)) {
1944      return;
1945    }
1946
1947    let draggedItemId =
1948      aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
1949
1950    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1951
1952    // DraggedWrapper might no longer available if a widget node is
1953    // destroyed after starting (but before stopping) a drag.
1954    if (draggedWrapper) {
1955      draggedWrapper.hidden = false;
1956      draggedWrapper.removeAttribute("mousedown");
1957    }
1958
1959    if (this._dragOverItem) {
1960      this._cancelDragActive(this._dragOverItem);
1961      this._dragOverItem = null;
1962    }
1963    this._updateToolbarCustomizationOutline(this.window);
1964    this._showPanelCustomizationPlaceholders();
1965    DragPositionManager.stop();
1966  },
1967
1968  _isUnwantedDragDrop: function(aEvent) {
1969    // The simulated events generated by synthesizeDragStart/synthesizeDrop in
1970    // mochitests are used only for testing whether the right data is being put
1971    // into the dataTransfer. Neither cause a real drop to occur, so they don't
1972    // set the source node. There isn't a means of testing real drag and drops,
1973    // so this pref skips the check but it should only be set by test code.
1974    if (this._skipSourceNodeCheck) {
1975      return false;
1976    }
1977
1978    /* Discard drag events that originated from a separate window to
1979       prevent content->chrome privilege escalations. */
1980    let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
1981    // mozSourceNode is null in the dragStart event handler or if
1982    // the drag event originated in an external application.
1983    return !mozSourceNode ||
1984           mozSourceNode.ownerGlobal != this.window;
1985  },
1986
1987  _setDragActive: function(aItem, aValue, aDraggedItemId, aInToolbar) {
1988    if (!aItem) {
1989      return;
1990    }
1991
1992    if (aItem.getAttribute("dragover") != aValue) {
1993      aItem.setAttribute("dragover", aValue);
1994
1995      let window = aItem.ownerGlobal;
1996      let draggedItem = window.document.getElementById(aDraggedItemId);
1997      if (!aInToolbar) {
1998        this._setGridDragActive(aItem, draggedItem, aValue);
1999      } else {
2000        let targetArea = this._getCustomizableParent(aItem);
2001        this._updateToolbarCustomizationOutline(window, targetArea);
2002        let makeSpaceImmediately = false;
2003        if (!gDraggingInToolbars.has(targetArea.id)) {
2004          gDraggingInToolbars.add(targetArea.id);
2005          let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItemId);
2006          let originArea = this._getCustomizableParent(draggedWrapper);
2007          makeSpaceImmediately = originArea == targetArea;
2008        }
2009        // Calculate width of the item when it'd be dropped in this position
2010        let width = this._getDragItemSize(aItem, draggedItem).width;
2011        let direction = window.getComputedStyle(aItem).direction;
2012        let prop, otherProp;
2013        // If we're inserting before in ltr, or after in rtl:
2014        if ((aValue == "before") == (direction == "ltr")) {
2015          prop = "borderLeftWidth";
2016          otherProp = "border-right-width";
2017        } else {
2018          // otherwise:
2019          prop = "borderRightWidth";
2020          otherProp = "border-left-width";
2021        }
2022        if (makeSpaceImmediately) {
2023          aItem.setAttribute("notransition", "true");
2024        }
2025        aItem.style[prop] = width + 'px';
2026        aItem.style.removeProperty(otherProp);
2027        if (makeSpaceImmediately) {
2028          // Force a layout flush:
2029          aItem.getBoundingClientRect();
2030          aItem.removeAttribute("notransition");
2031        }
2032      }
2033    }
2034  },
2035  _cancelDragActive: function(aItem, aNextItem, aNoTransition) {
2036    this._updateToolbarCustomizationOutline(aItem.ownerGlobal);
2037    let currentArea = this._getCustomizableParent(aItem);
2038    if (!currentArea) {
2039      return;
2040    }
2041    let isToolbar = CustomizableUI.getAreaType(currentArea.id) == "toolbar";
2042    if (isToolbar) {
2043      if (aNoTransition) {
2044        aItem.setAttribute("notransition", "true");
2045      }
2046      aItem.removeAttribute("dragover");
2047      // Remove both property values in the case that the end padding
2048      // had been set.
2049      aItem.style.removeProperty("border-left-width");
2050      aItem.style.removeProperty("border-right-width");
2051      if (aNoTransition) {
2052        // Force a layout flush:
2053        aItem.getBoundingClientRect();
2054        aItem.removeAttribute("notransition");
2055      }
2056    } else  {
2057      aItem.removeAttribute("dragover");
2058      if (aNextItem) {
2059        let nextArea = this._getCustomizableParent(aNextItem);
2060        if (nextArea == currentArea) {
2061          // No need to do anything if we're still dragging in this area:
2062          return;
2063        }
2064      }
2065      // Otherwise, clear everything out:
2066      let positionManager = DragPositionManager.getManagerForArea(currentArea);
2067      positionManager.clearPlaceholders(currentArea, aNoTransition);
2068    }
2069  },
2070
2071  _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) {
2072    let targetArea = this._getCustomizableParent(aDragOverNode);
2073    let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItem.id);
2074    let originArea = this._getCustomizableParent(draggedWrapper);
2075    let positionManager = DragPositionManager.getManagerForArea(targetArea);
2076    let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
2077    let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
2078    positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize,
2079                                      originArea == targetArea);
2080  },
2081
2082  _getDragItemSize: function(aDragOverNode, aDraggedItem) {
2083    // Cache it good, cache it real good.
2084    if (!this._dragSizeMap)
2085      this._dragSizeMap = new WeakMap();
2086    if (!this._dragSizeMap.has(aDraggedItem))
2087      this._dragSizeMap.set(aDraggedItem, new WeakMap());
2088    let itemMap = this._dragSizeMap.get(aDraggedItem);
2089    let targetArea = this._getCustomizableParent(aDragOverNode);
2090    let currentArea = this._getCustomizableParent(aDraggedItem);
2091    // Return the size for this target from cache, if it exists.
2092    let size = itemMap.get(targetArea);
2093    if (size)
2094      return size;
2095
2096    // Calculate size of the item when it'd be dropped in this position.
2097    let currentParent = aDraggedItem.parentNode;
2098    let currentSibling = aDraggedItem.nextSibling;
2099    const kAreaType = "cui-areatype";
2100    let areaType, currentType;
2101
2102    if (targetArea != currentArea) {
2103      // Move the widget temporarily next to the placeholder.
2104      aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
2105      // Update the node's areaType.
2106      areaType = CustomizableUI.getAreaType(targetArea.id);
2107      currentType = aDraggedItem.hasAttribute(kAreaType) &&
2108                    aDraggedItem.getAttribute(kAreaType);
2109      if (areaType)
2110        aDraggedItem.setAttribute(kAreaType, areaType);
2111      this.wrapToolbarItem(aDraggedItem, areaType || "palette");
2112      CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
2113    } else {
2114      aDraggedItem.parentNode.hidden = false;
2115    }
2116
2117    // Fetch the new size.
2118    let rect = aDraggedItem.parentNode.getBoundingClientRect();
2119    size = {width: rect.width, height: rect.height};
2120    // Cache the found value of size for this target.
2121    itemMap.set(targetArea, size);
2122
2123    if (targetArea != currentArea) {
2124      this.unwrapToolbarItem(aDraggedItem.parentNode);
2125      // Put the item back into its previous position.
2126      currentParent.insertBefore(aDraggedItem, currentSibling);
2127      // restore the areaType
2128      if (areaType) {
2129        if (currentType === false)
2130          aDraggedItem.removeAttribute(kAreaType);
2131        else
2132          aDraggedItem.setAttribute(kAreaType, currentType);
2133      }
2134      this.createOrUpdateWrapper(aDraggedItem, null, true);
2135      CustomizableUI.onWidgetDrag(aDraggedItem.id);
2136    } else {
2137      aDraggedItem.parentNode.hidden = true;
2138    }
2139    return size;
2140  },
2141
2142  _getCustomizableParent: function(aElement) {
2143    let areas = CustomizableUI.areas;
2144    areas.push(kPaletteId);
2145    while (aElement) {
2146      if (areas.indexOf(aElement.id) != -1) {
2147        return aElement;
2148      }
2149      aElement = aElement.parentNode;
2150    }
2151    return null;
2152  },
2153
2154  _getDragOverNode: function(aEvent, aAreaElement, aInToolbar, aDraggedItemId) {
2155    let expectedParent = aAreaElement.customizationTarget || aAreaElement;
2156    // Our tests are stupid. Cope:
2157    if (!aEvent.clientX  && !aEvent.clientY) {
2158      return aEvent.target;
2159    }
2160    // Offset the drag event's position with the offset to the center of
2161    // the thing we're dragging
2162    let dragX = aEvent.clientX - this._dragOffset.x;
2163    let dragY = aEvent.clientY - this._dragOffset.y;
2164
2165    // Ensure this is within the container
2166    let boundsContainer = expectedParent;
2167    // NB: because the panel UI itself is inside a scrolling container, we need
2168    // to use the parent bounds (otherwise, if the panel UI is scrolled down,
2169    // the numbers we get are in window coordinates which leads to various kinds
2170    // of weirdness)
2171    if (boundsContainer == this.panelUIContents) {
2172      boundsContainer = boundsContainer.parentNode;
2173    }
2174    let bounds = boundsContainer.getBoundingClientRect();
2175    dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
2176    dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
2177
2178    let targetNode;
2179    if (aInToolbar) {
2180      targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
2181      while (targetNode && targetNode.parentNode != expectedParent) {
2182        targetNode = targetNode.parentNode;
2183      }
2184    } else {
2185      let positionManager = DragPositionManager.getManagerForArea(aAreaElement);
2186      // Make it relative to the container:
2187      dragX -= bounds.left;
2188      // NB: but if we're in the panel UI, we need to use the actual panel
2189      // contents instead of the scrolling container to determine our origin
2190      // offset against:
2191      if (expectedParent == this.panelUIContents) {
2192        dragY -= this.panelUIContents.getBoundingClientRect().top;
2193      } else {
2194        dragY -= bounds.top;
2195      }
2196      // Find the closest node:
2197      targetNode = positionManager.find(aAreaElement, dragX, dragY, aDraggedItemId);
2198    }
2199    return targetNode || aEvent.target;
2200  },
2201
2202  _onMouseDown: function(aEvent) {
2203    log.debug("_onMouseDown");
2204    if (aEvent.button != 0) {
2205      return;
2206    }
2207    let doc = aEvent.target.ownerDocument;
2208    doc.documentElement.setAttribute("customizing-movingItem", true);
2209    let item = this._getWrapper(aEvent.target);
2210    if (item && !item.classList.contains(kPlaceholderClass) &&
2211        item.getAttribute("removable") == "true") {
2212      item.setAttribute("mousedown", "true");
2213    }
2214  },
2215
2216  _onMouseUp: function(aEvent) {
2217    log.debug("_onMouseUp");
2218    if (aEvent.button != 0) {
2219      return;
2220    }
2221    let doc = aEvent.target.ownerDocument;
2222    doc.documentElement.removeAttribute("customizing-movingItem");
2223    let item = this._getWrapper(aEvent.target);
2224    if (item) {
2225      item.removeAttribute("mousedown");
2226    }
2227  },
2228
2229  _getWrapper: function(aElement) {
2230    while (aElement && aElement.localName != "toolbarpaletteitem") {
2231      if (aElement.localName == "toolbar")
2232        return null;
2233      aElement = aElement.parentNode;
2234    }
2235    return aElement;
2236  },
2237
2238  _showPanelCustomizationPlaceholders: function() {
2239    let doc = this.document;
2240    let contents = this.panelUIContents;
2241    let narrowItemsAfterWideItem = 0;
2242    let node = contents.lastChild;
2243    while (node && !node.classList.contains(CustomizableUI.WIDE_PANEL_CLASS) &&
2244           (!node.firstChild || !node.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS))) {
2245      if (!node.hidden && !node.classList.contains(kPlaceholderClass)) {
2246        narrowItemsAfterWideItem++;
2247      }
2248      node = node.previousSibling;
2249    }
2250
2251    let orphanedItems = narrowItemsAfterWideItem % CustomizableUI.PANEL_COLUMN_COUNT;
2252    let placeholders = CustomizableUI.PANEL_COLUMN_COUNT - orphanedItems;
2253
2254    let currentPlaceholderCount = contents.querySelectorAll("." + kPlaceholderClass).length;
2255    if (placeholders > currentPlaceholderCount) {
2256      while (placeholders-- > currentPlaceholderCount) {
2257        let placeholder = doc.createElement("toolbarpaletteitem");
2258        placeholder.classList.add(kPlaceholderClass);
2259        // XXXjaws The toolbarbutton child here is only necessary to get
2260        //  the styling right here.
2261        let placeholderChild = doc.createElement("toolbarbutton");
2262        placeholderChild.classList.add(kPlaceholderClass + "-child");
2263        placeholder.appendChild(placeholderChild);
2264        contents.appendChild(placeholder);
2265      }
2266    } else if (placeholders < currentPlaceholderCount) {
2267      while (placeholders++ < currentPlaceholderCount) {
2268        contents.querySelectorAll("." + kPlaceholderClass)[0].remove();
2269      }
2270    }
2271  },
2272
2273  _removePanelCustomizationPlaceholders: function() {
2274    let contents = this.panelUIContents;
2275    let oldPlaceholders = contents.getElementsByClassName(kPlaceholderClass);
2276    while (oldPlaceholders.length) {
2277      contents.removeChild(oldPlaceholders[0]);
2278    }
2279  },
2280
2281  /**
2282   * Update toolbar customization targets during drag events to add or remove
2283   * outlines to indicate that an area is customizable.
2284   *
2285   * @param aWindow                       The XUL window in which outlines should be updated.
2286   * @param {Element} [aToolbarArea=null] The element of the customizable toolbar area to add the
2287   *                                      outline to. If aToolbarArea is falsy, the outline will be
2288   *                                      removed from all toolbar areas.
2289   */
2290  _updateToolbarCustomizationOutline: function(aWindow, aToolbarArea = null) {
2291    // Remove the attribute from existing customization targets
2292    for (let area of CustomizableUI.areas) {
2293      if (CustomizableUI.getAreaType(area) != CustomizableUI.TYPE_TOOLBAR) {
2294        continue;
2295      }
2296      let target = CustomizableUI.getCustomizeTargetForArea(area, aWindow);
2297      target.removeAttribute("customizing-dragovertarget");
2298    }
2299
2300    // Now set the attribute on the desired target
2301    if (aToolbarArea) {
2302      if (CustomizableUI.getAreaType(aToolbarArea.id) != CustomizableUI.TYPE_TOOLBAR)
2303        return;
2304      let target = CustomizableUI.getCustomizeTargetForArea(aToolbarArea.id, aWindow);
2305      target.setAttribute("customizing-dragovertarget", true);
2306    }
2307  },
2308
2309  _findVisiblePreviousSiblingNode: function(aReferenceNode) {
2310    while (aReferenceNode &&
2311           aReferenceNode.localName == "toolbarpaletteitem" &&
2312           aReferenceNode.firstChild.hidden) {
2313      aReferenceNode = aReferenceNode.previousSibling;
2314    }
2315    return aReferenceNode;
2316  },
2317};
2318
2319function __dumpDragData(aEvent, caller) {
2320  if (!gDebug) {
2321    return;
2322  }
2323  let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
2324  str += "  type: " + aEvent["type"] + "\n";
2325  for (let el of ["target", "currentTarget", "relatedTarget"]) {
2326    if (aEvent[el]) {
2327      str += "  " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n";
2328    }
2329  }
2330  for (let prop in aEvent.dataTransfer) {
2331    if (typeof aEvent.dataTransfer[prop] != "function") {
2332      str += "  dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
2333    }
2334  }
2335  str += "}";
2336  log.debug(str);
2337}
2338
2339function dispatchFunction(aFunc) {
2340  Services.tm.currentThread.dispatch(aFunc, Ci.nsIThread.DISPATCH_NORMAL);
2341}
2342