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