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"use strict"; 5 6var EXPORTED_SYMBOLS = ["CustomizableUI"]; 7 8const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 9const { XPCOMUtils } = ChromeUtils.import( 10 "resource://gre/modules/XPCOMUtils.jsm" 11); 12const { AppConstants } = ChromeUtils.import( 13 "resource://gre/modules/AppConstants.jsm" 14); 15 16XPCOMUtils.defineLazyModuleGetters(this, { 17 AddonManager: "resource://gre/modules/AddonManager.jsm", 18 AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm", 19 SearchWidgetTracker: "resource:///modules/SearchWidgetTracker.jsm", 20 CustomizableWidgets: "resource:///modules/CustomizableWidgets.jsm", 21 PanelMultiView: "resource:///modules/PanelMultiView.jsm", 22 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", 23 ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm", 24 BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.jsm", 25}); 26 27XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { 28 const kUrl = 29 "chrome://browser/locale/customizableui/customizableWidgets.properties"; 30 return Services.strings.createBundle(kUrl); 31}); 32 33XPCOMUtils.defineLazyServiceGetter( 34 this, 35 "gELS", 36 "@mozilla.org/eventlistenerservice;1", 37 "nsIEventListenerService" 38); 39 40const kDefaultThemeID = "default-theme@mozilla.org"; 41 42const kSpecialWidgetPfx = "customizableui-special-"; 43 44const kPrefCustomizationState = "browser.uiCustomization.state"; 45const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; 46const kPrefCustomizationDebug = "browser.uiCustomization.debug"; 47const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar"; 48const kPrefExtraDragSpace = "browser.tabs.extraDragSpace"; 49const kPrefUIDensity = "browser.uidensity"; 50const kPrefAutoTouchMode = "browser.touchmode.auto"; 51const kPrefAutoHideDownloadsButton = "browser.download.autohideButton"; 52 53const kExpectedWindowURL = AppConstants.BROWSER_CHROME_URL; 54 55var gDefaultTheme; 56var gSelectedTheme; 57 58/** 59 * The keys are the handlers that are fired when the event type (the value) 60 * is fired on the subview. A widget that provides a subview has the option 61 * of providing onViewShowing and onViewHiding event handlers. 62 */ 63const kSubviewEvents = ["ViewShowing", "ViewHiding"]; 64 65/** 66 * The current version. We can use this to auto-add new default widgets as necessary. 67 * (would be const but isn't because of testing purposes) 68 */ 69var kVersion = 16; 70 71/** 72 * Buttons removed from built-ins by version they were removed. kVersion must be 73 * bumped any time a new id is added to this. Use the button id as key, and 74 * version the button is removed in as the value. e.g. "pocket-button": 5 75 */ 76var ObsoleteBuiltinButtons = { 77 "feed-button": 15, 78}; 79 80/** 81 * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed 82 * on their IDs. 83 */ 84var gPalette = new Map(); 85 86/** 87 * gAreas maps area IDs to Sets of properties about those areas. An area is a 88 * place where a widget can be put. 89 */ 90var gAreas = new Map(); 91 92/** 93 * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets 94 * are placed within that area (either directly in the area node, or in the 95 * customizationTarget of the node). 96 */ 97var gPlacements = new Map(); 98 99/** 100 * gFuturePlacements represent placements that will happen for areas that have 101 * not yet loaded (due to lazy-loading). This can occur when add-ons register 102 * widgets. 103 */ 104var gFuturePlacements = new Map(); 105 106// XXXunf Temporary. Need a nice way to abstract functions to build widgets 107// of these types. 108var gSupportedWidgetTypes = new Set(["button", "view", "custom"]); 109 110/** 111 * gPanelsForWindow is a list of known panels in a window which we may need to close 112 * should command events fire which target them. 113 */ 114var gPanelsForWindow = new WeakMap(); 115 116/** 117 * gSeenWidgets remembers which widgets the user has seen for the first time 118 * before. This way, if a new widget is created, and the user has not seen it 119 * before, it can be put in its default location. Otherwise, it remains in the 120 * palette. 121 */ 122var gSeenWidgets = new Set(); 123 124/** 125 * gDirtyAreaCache is a set of area IDs for areas where items have been added, 126 * moved or removed at least once. This set is persisted, and is used to 127 * optimize building of toolbars in the default case where no toolbars should 128 * be "dirty". 129 */ 130var gDirtyAreaCache = new Set(); 131 132/** 133 * gPendingBuildAreas is a map from area IDs to map from build nodes to their 134 * existing children at the time of node registration, that are waiting 135 * for the area to be registered 136 */ 137var gPendingBuildAreas = new Map(); 138 139var gSavedState = null; 140var gRestoring = false; 141var gDirty = false; 142var gInBatchStack = 0; 143var gResetting = false; 144var gUndoResetting = false; 145 146/** 147 * gBuildAreas maps area IDs to actual area nodes within browser windows. 148 */ 149var gBuildAreas = new Map(); 150 151/** 152 * gBuildWindows is a map of windows that have registered build areas, mapped 153 * to a Set of known toolboxes in that window. 154 */ 155var gBuildWindows = new Map(); 156 157var gNewElementCount = 0; 158var gGroupWrapperCache = new Map(); 159var gSingleWrapperCache = new WeakMap(); 160var gListeners = new Set(); 161 162var gUIStateBeforeReset = { 163 uiCustomizationState: null, 164 drawInTitlebar: null, 165 extraDragSpace: null, 166 currentTheme: null, 167 uiDensity: null, 168 autoTouchMode: null, 169}; 170 171XPCOMUtils.defineLazyPreferenceGetter( 172 this, 173 "gDebuggingEnabled", 174 kPrefCustomizationDebug, 175 false, 176 (pref, oldVal, newVal) => { 177 if (typeof log != "undefined") { 178 log.maxLogLevel = newVal ? "all" : "log"; 179 } 180 } 181); 182 183XPCOMUtils.defineLazyGetter(this, "log", () => { 184 let scope = {}; 185 ChromeUtils.import("resource://gre/modules/Console.jsm", scope); 186 let consoleOptions = { 187 maxLogLevel: gDebuggingEnabled ? "all" : "log", 188 prefix: "CustomizableUI", 189 }; 190 return new scope.ConsoleAPI(consoleOptions); 191}); 192 193var CustomizableUIInternal = { 194 initialize() { 195 log.debug("Initializing"); 196 197 AddonManagerPrivate.databaseReady.then(async () => { 198 AddonManager.addAddonListener(this); 199 200 let addons = await AddonManager.getAddonsByTypes(["theme"]); 201 gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID); 202 gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme; 203 }); 204 205 this.addListener(this); 206 this._defineBuiltInWidgets(); 207 this.loadSavedState(); 208 this._updateForNewVersion(); 209 this._markObsoleteBuiltinButtonsSeen(); 210 211 this.registerArea( 212 CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, 213 { 214 type: CustomizableUI.TYPE_MENU_PANEL, 215 defaultPlacements: [], 216 anchor: "nav-bar-overflow-button", 217 }, 218 true 219 ); 220 221 let navbarPlacements = [ 222 "back-button", 223 "forward-button", 224 "stop-reload-button", 225 "home-button", 226 "spring", 227 "urlbar-container", 228 "spring", 229 "downloads-button", 230 "library-button", 231 "sidebar-button", 232 "fxa-toolbar-menu-button", 233 ]; 234 235 if (AppConstants.MOZ_DEV_EDITION) { 236 navbarPlacements.splice(7, 0, "developer-button"); 237 } 238 239 this.registerArea( 240 CustomizableUI.AREA_NAVBAR, 241 { 242 type: CustomizableUI.TYPE_TOOLBAR, 243 overflowable: true, 244 defaultPlacements: navbarPlacements, 245 defaultCollapsed: false, 246 }, 247 true 248 ); 249 250 if (AppConstants.MENUBAR_CAN_AUTOHIDE) { 251 this.registerArea( 252 CustomizableUI.AREA_MENUBAR, 253 { 254 type: CustomizableUI.TYPE_TOOLBAR, 255 defaultPlacements: ["menubar-items"], 256 defaultCollapsed: true, 257 }, 258 true 259 ); 260 } 261 262 this.registerArea( 263 CustomizableUI.AREA_TABSTRIP, 264 { 265 type: CustomizableUI.TYPE_TOOLBAR, 266 defaultPlacements: [ 267 "tabbrowser-tabs", 268 "new-tab-button", 269 "alltabs-button", 270 ], 271 defaultCollapsed: null, 272 }, 273 true 274 ); 275 this.registerArea( 276 CustomizableUI.AREA_BOOKMARKS, 277 { 278 type: CustomizableUI.TYPE_TOOLBAR, 279 defaultPlacements: ["personal-bookmarks"], 280 defaultCollapsed: true, 281 }, 282 true 283 ); 284 285 SearchWidgetTracker.init(); 286 }, 287 288 onEnabled(addon) { 289 if (addon.type == "theme") { 290 gSelectedTheme = addon; 291 } 292 }, 293 294 get _builtinAreas() { 295 return new Set([ 296 ...this._builtinToolbars, 297 CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, 298 ]); 299 }, 300 301 get _builtinToolbars() { 302 let toolbars = new Set([ 303 CustomizableUI.AREA_NAVBAR, 304 CustomizableUI.AREA_BOOKMARKS, 305 CustomizableUI.AREA_TABSTRIP, 306 ]); 307 if (AppConstants.platform != "macosx") { 308 toolbars.add(CustomizableUI.AREA_MENUBAR); 309 } 310 return toolbars; 311 }, 312 313 _defineBuiltInWidgets() { 314 for (let widgetDefinition of CustomizableWidgets) { 315 this.createBuiltinWidget(widgetDefinition); 316 } 317 }, 318 319 // eslint-disable-next-line complexity 320 _updateForNewVersion() { 321 // We should still enter even if gSavedState.currentVersion >= kVersion 322 // because the per-widget pref facility is independent of versioning. 323 if (!gSavedState) { 324 // Flip all the prefs so we don't try to re-introduce later: 325 for (let [, widget] of gPalette) { 326 if (widget.defaultArea && widget._introducedInVersion === "pref") { 327 let prefId = "browser.toolbarbuttons.introduced." + widget.id; 328 Services.prefs.setBoolPref(prefId, true); 329 } 330 } 331 return; 332 } 333 334 let currentVersion = gSavedState.currentVersion; 335 for (let [id, widget] of gPalette) { 336 if (widget.defaultArea) { 337 let shouldAdd = false; 338 let shouldSetPref = false; 339 let prefId = "browser.toolbarbuttons.introduced." + widget.id; 340 if (widget._introducedInVersion === "pref") { 341 try { 342 shouldAdd = !Services.prefs.getBoolPref(prefId); 343 } catch (ex) { 344 // Pref doesn't exist: 345 shouldAdd = true; 346 } 347 shouldSetPref = shouldAdd; 348 } else if (widget._introducedInVersion > currentVersion) { 349 shouldAdd = true; 350 } 351 352 if (shouldAdd) { 353 let futurePlacements = gFuturePlacements.get(widget.defaultArea); 354 if (futurePlacements) { 355 futurePlacements.add(id); 356 } else { 357 gFuturePlacements.set(widget.defaultArea, new Set([id])); 358 } 359 if (shouldSetPref) { 360 Services.prefs.setBoolPref(prefId, true); 361 } 362 } 363 } 364 } 365 366 if ( 367 currentVersion < 7 && 368 gSavedState.placements && 369 gSavedState.placements[CustomizableUI.AREA_NAVBAR] 370 ) { 371 let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 372 let newPlacements = [ 373 "back-button", 374 "forward-button", 375 "stop-reload-button", 376 "home-button", 377 ]; 378 for (let button of placements) { 379 if (!newPlacements.includes(button)) { 380 newPlacements.push(button); 381 } 382 } 383 384 if (!newPlacements.includes("sidebar-button")) { 385 newPlacements.push("sidebar-button"); 386 } 387 388 gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements; 389 } 390 391 if ( 392 currentVersion < 8 && 393 gSavedState.placements && 394 gSavedState.placements["PanelUI-contents"] 395 ) { 396 let savedPanelPlacements = gSavedState.placements["PanelUI-contents"]; 397 delete gSavedState.placements["PanelUI-contents"]; 398 let defaultPlacements = [ 399 "edit-controls", 400 "zoom-controls", 401 "new-window-button", 402 "privatebrowsing-button", 403 "save-page-button", 404 "print-button", 405 "history-panelmenu", 406 "fullscreen-button", 407 "find-button", 408 "preferences-button", 409 "add-ons-button", 410 "sync-button", 411 ]; 412 413 if (!AppConstants.MOZ_DEV_EDITION) { 414 defaultPlacements.splice(-1, 0, "developer-button"); 415 } 416 417 let showCharacterEncoding = Services.prefs.getComplexValue( 418 "browser.menu.showCharacterEncoding", 419 Ci.nsIPrefLocalizedString 420 ).data; 421 if (showCharacterEncoding == "true") { 422 defaultPlacements.push("characterencoding-button"); 423 } 424 425 savedPanelPlacements = savedPanelPlacements.filter( 426 id => !defaultPlacements.includes(id) 427 ); 428 429 if (savedPanelPlacements.length) { 430 gSavedState.placements[ 431 CustomizableUI.AREA_FIXED_OVERFLOW_PANEL 432 ] = savedPanelPlacements; 433 } 434 } 435 436 if ( 437 currentVersion < 9 && 438 gSavedState.placements && 439 gSavedState.placements["nav-bar"] 440 ) { 441 let placements = gSavedState.placements["nav-bar"]; 442 if (placements.includes("urlbar-container")) { 443 let urlbarIndex = placements.indexOf("urlbar-container"); 444 let secondSpringIndex = urlbarIndex + 1; 445 // Insert if there isn't already a spring before the urlbar 446 if ( 447 urlbarIndex == 0 || 448 !placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring") 449 ) { 450 placements.splice(urlbarIndex, 0, "spring"); 451 // The url bar is now 1 index later, so increment the insertion point for 452 // the second spring. 453 secondSpringIndex++; 454 } 455 // If the search container is present, insert after the search container 456 // instead of after the url bar 457 let searchContainerIndex = placements.indexOf("search-container"); 458 if (searchContainerIndex != -1) { 459 secondSpringIndex = searchContainerIndex + 1; 460 } 461 if ( 462 secondSpringIndex == placements.length || 463 !placements[secondSpringIndex].startsWith( 464 kSpecialWidgetPfx + "spring" 465 ) 466 ) { 467 placements.splice(secondSpringIndex, 0, "spring"); 468 } 469 } 470 471 // Finally, replace the bookmarks menu button with the library one if present 472 if (placements.includes("bookmarks-menu-button")) { 473 let bmbIndex = placements.indexOf("bookmarks-menu-button"); 474 placements.splice(bmbIndex, 1); 475 let downloadButtonIndex = placements.indexOf("downloads-button"); 476 let libraryIndex = 477 downloadButtonIndex == -1 ? bmbIndex : downloadButtonIndex + 1; 478 placements.splice(libraryIndex, 0, "library-button"); 479 } 480 } 481 482 if (currentVersion < 10 && gSavedState.placements) { 483 for (let placements of Object.values(gSavedState.placements)) { 484 if (placements.includes("webcompat-reporter-button")) { 485 placements.splice(placements.indexOf("webcompat-reporter-button"), 1); 486 break; 487 } 488 } 489 } 490 491 // Move the downloads button to the default position in the navbar if it's 492 // not there already. 493 if (currentVersion < 11 && gSavedState.placements) { 494 let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 495 // First remove from wherever it currently lives, if anywhere: 496 for (let placements of Object.values(gSavedState.placements)) { 497 let existingIndex = placements.indexOf("downloads-button"); 498 if (existingIndex != -1) { 499 placements.splice(existingIndex, 1); 500 break; // It can only be in 1 place, so no point looking elsewhere. 501 } 502 } 503 504 // Now put the button in the navbar in the correct spot: 505 if (navbarPlacements) { 506 let insertionPoint = navbarPlacements.indexOf("urlbar-container"); 507 // Deliberately iterate to 1 past the end of the array to insert at the 508 // end if need be. 509 while (++insertionPoint < navbarPlacements.length) { 510 let widget = navbarPlacements[insertionPoint]; 511 // If we find a non-searchbar, non-spacer node, break out of the loop: 512 if ( 513 widget != "search-container" && 514 !this.matchingSpecials(widget, "spring") 515 ) { 516 break; 517 } 518 } 519 // We either found the right spot, or reached the end of the 520 // placements, so insert here: 521 navbarPlacements.splice(insertionPoint, 0, "downloads-button"); 522 } 523 } 524 525 if (currentVersion < 12 && gSavedState.placements) { 526 const removedButtons = [ 527 "loop-call-button", 528 "loop-button-throttled", 529 "pocket-button", 530 ]; 531 for (let placements of Object.values(gSavedState.placements)) { 532 for (let button of removedButtons) { 533 let buttonIndex = placements.indexOf(button); 534 if (buttonIndex != -1) { 535 placements.splice(buttonIndex, 1); 536 } 537 } 538 } 539 } 540 541 // Remove the old placements from the now-gone Nightly-only 542 // "New non-e10s window" button. 543 if (currentVersion < 13 && gSavedState.placements) { 544 for (let placements of Object.values(gSavedState.placements)) { 545 let buttonIndex = placements.indexOf("e10s-button"); 546 if (buttonIndex != -1) { 547 placements.splice(buttonIndex, 1); 548 } 549 } 550 } 551 552 // Remove unsupported custom toolbar saved placements 553 if (currentVersion < 14 && gSavedState.placements) { 554 for (let area in gSavedState.placements) { 555 if (!this._builtinAreas.has(area)) { 556 delete gSavedState.placements[area]; 557 } 558 } 559 } 560 561 // Add the FxA toolbar menu as the right most button item 562 if (currentVersion < 16 && gSavedState.placements) { 563 let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 564 // Place the menu item as the first item to the left of the hamburger menu 565 if (navbarPlacements) { 566 navbarPlacements.push("fxa-toolbar-menu-button"); 567 } 568 } 569 }, 570 571 /** 572 * _markObsoleteBuiltinButtonsSeen 573 * when upgrading, ensure obsoleted buttons are in seen state. 574 */ 575 _markObsoleteBuiltinButtonsSeen() { 576 if (!gSavedState) { 577 return; 578 } 579 let currentVersion = gSavedState.currentVersion; 580 if (currentVersion >= kVersion) { 581 return; 582 } 583 // we're upgrading, update state if necessary 584 for (let id in ObsoleteBuiltinButtons) { 585 let version = ObsoleteBuiltinButtons[id]; 586 if (version == kVersion) { 587 gSeenWidgets.add(id); 588 gDirty = true; 589 } 590 } 591 }, 592 593 _placeNewDefaultWidgetsInArea(aArea) { 594 let futurePlacedWidgets = gFuturePlacements.get(aArea); 595 let savedPlacements = 596 gSavedState && gSavedState.placements && gSavedState.placements[aArea]; 597 let defaultPlacements = gAreas.get(aArea).get("defaultPlacements"); 598 if ( 599 !savedPlacements || 600 !savedPlacements.length || 601 !futurePlacedWidgets || 602 !defaultPlacements || 603 !defaultPlacements.length 604 ) { 605 return; 606 } 607 let defaultWidgetIndex = -1; 608 609 for (let widgetId of futurePlacedWidgets) { 610 let widget = gPalette.get(widgetId); 611 if ( 612 !widget || 613 widget.source !== CustomizableUI.SOURCE_BUILTIN || 614 !widget.defaultArea || 615 !widget._introducedInVersion || 616 savedPlacements.includes(widget.id) 617 ) { 618 continue; 619 } 620 defaultWidgetIndex = defaultPlacements.indexOf(widget.id); 621 if (defaultWidgetIndex === -1) { 622 continue; 623 } 624 // Now we know that this widget should be here by default, was newly introduced, 625 // and we have a saved state to insert into, and a default state to work off of. 626 // Try introducing after widgets that come before it in the default placements: 627 for (let i = defaultWidgetIndex; i >= 0; i--) { 628 // Special case: if the defaults list this widget as coming first, insert at the beginning: 629 if (i === 0 && i === defaultWidgetIndex) { 630 savedPlacements.splice(0, 0, widget.id); 631 // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is 632 // safe, and we won't skip any items. 633 futurePlacedWidgets.delete(widget.id); 634 gDirty = true; 635 break; 636 } 637 // Otherwise, if we're somewhere other than the beginning, check if the previous 638 // widget is in the saved placements. 639 if (i) { 640 let previousWidget = defaultPlacements[i - 1]; 641 let previousWidgetIndex = savedPlacements.indexOf(previousWidget); 642 if (previousWidgetIndex != -1) { 643 savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id); 644 futurePlacedWidgets.delete(widget.id); 645 gDirty = true; 646 break; 647 } 648 } 649 } 650 // The loop above either inserts the item or doesn't - either way, we can get away 651 // with doing nothing else now; if the item remains in gFuturePlacements, we'll 652 // add it at the end in restoreStateForArea. 653 } 654 this.saveState(); 655 }, 656 657 getCustomizationTarget(aElement) { 658 if (!aElement) { 659 return null; 660 } 661 662 if ( 663 !aElement._customizationTarget && 664 aElement.hasAttribute("customizable") 665 ) { 666 let id = aElement.getAttribute("customizationtarget"); 667 if (id) { 668 aElement._customizationTarget = aElement.ownerDocument.getElementById( 669 id 670 ); 671 } 672 673 if (!aElement._customizationTarget) { 674 aElement._customizationTarget = aElement; 675 } 676 } 677 678 return aElement._customizationTarget; 679 }, 680 681 wrapWidget(aWidgetId) { 682 if (gGroupWrapperCache.has(aWidgetId)) { 683 return gGroupWrapperCache.get(aWidgetId); 684 } 685 686 let provider = this.getWidgetProvider(aWidgetId); 687 if (!provider) { 688 return null; 689 } 690 691 if (provider == CustomizableUI.PROVIDER_API) { 692 let widget = gPalette.get(aWidgetId); 693 if (!widget.wrapper) { 694 widget.wrapper = new WidgetGroupWrapper(widget); 695 gGroupWrapperCache.set(aWidgetId, widget.wrapper); 696 } 697 return widget.wrapper; 698 } 699 700 // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. 701 // XXXgijs: this causes bugs in code that depends on widgetWrapper.provider 702 // giving an accurate answer... filed as bug 1379821 703 let wrapper = new XULWidgetGroupWrapper(aWidgetId); 704 gGroupWrapperCache.set(aWidgetId, wrapper); 705 return wrapper; 706 }, 707 708 registerArea(aName, aProperties, aInternalCaller) { 709 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { 710 throw new Error("Invalid area name"); 711 } 712 713 let areaIsKnown = gAreas.has(aName); 714 let props = areaIsKnown ? gAreas.get(aName) : new Map(); 715 const kImmutableProperties = new Set(["type", "overflowable"]); 716 for (let key in aProperties) { 717 if ( 718 areaIsKnown && 719 kImmutableProperties.has(key) && 720 props.get(key) != aProperties[key] 721 ) { 722 throw new Error("An area cannot change the property for '" + key + "'"); 723 } 724 props.set(key, aProperties[key]); 725 } 726 // Default to a toolbar: 727 if (!props.has("type")) { 728 props.set("type", CustomizableUI.TYPE_TOOLBAR); 729 } 730 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 731 // Check aProperties instead of props because this check is only interested 732 // in the passed arguments, not the state of a potentially pre-existing area. 733 if (!aInternalCaller && aProperties.defaultCollapsed) { 734 throw new Error( 735 "defaultCollapsed is only allowed for default toolbars." 736 ); 737 } 738 if (!props.has("defaultCollapsed")) { 739 props.set("defaultCollapsed", true); 740 } 741 } else if (props.has("defaultCollapsed")) { 742 throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); 743 } 744 // Sanity check type: 745 let allTypes = [ 746 CustomizableUI.TYPE_TOOLBAR, 747 CustomizableUI.TYPE_MENU_PANEL, 748 ]; 749 if (!allTypes.includes(props.get("type"))) { 750 throw new Error("Invalid area type " + props.get("type")); 751 } 752 753 // And to no placements: 754 if (!props.has("defaultPlacements")) { 755 props.set("defaultPlacements", []); 756 } 757 // Sanity check default placements array: 758 if (!Array.isArray(props.get("defaultPlacements"))) { 759 throw new Error("Should provide an array of default placements"); 760 } 761 762 if (!areaIsKnown) { 763 gAreas.set(aName, props); 764 765 // Reconcile new default widgets. Have to do this before we start restoring things. 766 this._placeNewDefaultWidgetsInArea(aName); 767 768 if ( 769 props.get("type") == CustomizableUI.TYPE_TOOLBAR && 770 !gPlacements.has(aName) 771 ) { 772 // Guarantee this area exists in gFuturePlacements, to avoid checking it in 773 // various places elsewhere. 774 if (!gFuturePlacements.has(aName)) { 775 gFuturePlacements.set(aName, new Set()); 776 } 777 } else { 778 this.restoreStateForArea(aName); 779 } 780 781 // If we have pending build area nodes, register all of them 782 if (gPendingBuildAreas.has(aName)) { 783 let pendingNodes = gPendingBuildAreas.get(aName); 784 for (let pendingNode of pendingNodes) { 785 this.registerToolbarNode(pendingNode); 786 } 787 gPendingBuildAreas.delete(aName); 788 } 789 } 790 }, 791 792 unregisterArea(aName, aDestroyPlacements) { 793 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { 794 throw new Error("Invalid area name"); 795 } 796 if (!gAreas.has(aName) && !gPlacements.has(aName)) { 797 throw new Error("Area not registered"); 798 } 799 800 // Move all the widgets out 801 this.beginBatchUpdate(); 802 try { 803 let placements = gPlacements.get(aName); 804 if (placements) { 805 // Need to clone this array so removeWidgetFromArea doesn't modify it 806 placements = [...placements]; 807 placements.forEach(this.removeWidgetFromArea, this); 808 } 809 810 // Delete all remaining traces. 811 gAreas.delete(aName); 812 // Only destroy placements when necessary: 813 if (aDestroyPlacements) { 814 gPlacements.delete(aName); 815 } else { 816 // Otherwise we need to re-set them, as removeFromArea will have emptied 817 // them out: 818 gPlacements.set(aName, placements); 819 } 820 gFuturePlacements.delete(aName); 821 let existingAreaNodes = gBuildAreas.get(aName); 822 if (existingAreaNodes) { 823 for (let areaNode of existingAreaNodes) { 824 this.notifyListeners( 825 "onAreaNodeUnregistered", 826 aName, 827 this.getCustomizationTarget(areaNode), 828 CustomizableUI.REASON_AREA_UNREGISTERED 829 ); 830 } 831 } 832 gBuildAreas.delete(aName); 833 } finally { 834 this.endBatchUpdate(true); 835 } 836 }, 837 838 registerToolbarNode(aToolbar) { 839 let area = aToolbar.id; 840 if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { 841 return; 842 } 843 let areaProperties = gAreas.get(area); 844 845 // If this area is not registered, try to do it automatically: 846 if (!areaProperties) { 847 if (!gPendingBuildAreas.has(area)) { 848 gPendingBuildAreas.set(area, []); 849 } 850 gPendingBuildAreas.get(area).push(aToolbar); 851 return; 852 } 853 854 this.beginBatchUpdate(); 855 try { 856 let placements = gPlacements.get(area); 857 if ( 858 !placements && 859 areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR 860 ) { 861 this.restoreStateForArea(area); 862 placements = gPlacements.get(area); 863 } 864 865 // For toolbars that need it, mark as dirty. 866 let defaultPlacements = areaProperties.get("defaultPlacements"); 867 if ( 868 !this._builtinToolbars.has(area) || 869 placements.length != defaultPlacements.length || 870 !placements.every((id, i) => id == defaultPlacements[i]) 871 ) { 872 gDirtyAreaCache.add(area); 873 } 874 875 if (areaProperties.has("overflowable")) { 876 aToolbar.overflowable = new OverflowableToolbar(aToolbar); 877 } 878 879 this.registerBuildArea(area, aToolbar); 880 881 // We only build the toolbar if it's been marked as "dirty". Dirty means 882 // one of the following things: 883 // 1) Items have been added, moved or removed from this toolbar before. 884 // 2) The number of children of the toolbar does not match the length of 885 // the placements array for that area. 886 // 887 // This notion of being "dirty" is stored in a cache which is persisted 888 // in the saved state. 889 if (gDirtyAreaCache.has(area)) { 890 this.buildArea(area, placements, aToolbar); 891 } else { 892 // We must have a builtin toolbar that's in the default state. We need 893 // to only make sure that all the special nodes are correct. 894 let specials = placements.filter(p => this.isSpecialWidget(p)); 895 if (specials.length) { 896 this.updateSpecialsForBuiltinToolbar(aToolbar, specials); 897 } 898 } 899 this.notifyListeners( 900 "onAreaNodeRegistered", 901 area, 902 this.getCustomizationTarget(aToolbar) 903 ); 904 } finally { 905 this.endBatchUpdate(); 906 } 907 }, 908 909 updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) { 910 // Nodes are going to be in the correct order, so we can do this straightforwardly: 911 let { children } = this.getCustomizationTarget(aToolbar); 912 for (let kid of children) { 913 if ( 914 this.matchingSpecials(aSpecialIDs[0], kid) && 915 kid.getAttribute("skipintoolbarset") != "true" 916 ) { 917 kid.id = aSpecialIDs.shift(); 918 } 919 if (!aSpecialIDs.length) { 920 return; 921 } 922 } 923 }, 924 925 buildArea(aArea, aPlacements, aAreaNode) { 926 let document = aAreaNode.ownerDocument; 927 let window = document.defaultView; 928 let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); 929 let container = this.getCustomizationTarget(aAreaNode); 930 let areaIsPanel = 931 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL; 932 933 if (!container) { 934 throw new Error( 935 "Expected area " + aArea + " to have a customizationTarget attribute." 936 ); 937 } 938 939 // Restore nav-bar visibility since it may have been hidden 940 // through a migration path (bug 938980) or an add-on. 941 if (aArea == CustomizableUI.AREA_NAVBAR) { 942 aAreaNode.collapsed = false; 943 } 944 945 this.beginBatchUpdate(); 946 947 try { 948 let currentNode = container.firstElementChild; 949 let placementsToRemove = new Set(); 950 for (let id of aPlacements) { 951 while ( 952 currentNode && 953 currentNode.getAttribute("skipintoolbarset") == "true" 954 ) { 955 currentNode = currentNode.nextElementSibling; 956 } 957 958 // Fix ids for specials and continue, for correctly placed specials. 959 if ( 960 currentNode && 961 (!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) && 962 this.matchingSpecials(id, currentNode) 963 ) { 964 currentNode.id = id; 965 } 966 if (currentNode && currentNode.id == id) { 967 currentNode = currentNode.nextElementSibling; 968 continue; 969 } 970 971 if (this.isSpecialWidget(id) && areaIsPanel) { 972 placementsToRemove.add(id); 973 continue; 974 } 975 976 let [provider, node] = this.getWidgetNode(id, window); 977 if (!node) { 978 log.debug("Unknown widget: " + id); 979 continue; 980 } 981 982 let widget = null; 983 // If the placements have items in them which are (now) no longer removable, 984 // we shouldn't be moving them: 985 if (provider == CustomizableUI.PROVIDER_API) { 986 widget = gPalette.get(id); 987 if (!widget.removable && aArea != widget.defaultArea) { 988 placementsToRemove.add(id); 989 continue; 990 } 991 } else if ( 992 provider == CustomizableUI.PROVIDER_XUL && 993 node.parentNode != container && 994 !this.isWidgetRemovable(node) 995 ) { 996 placementsToRemove.add(id); 997 continue; 998 } // Special widgets are always removable, so no need to check them 999 1000 if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) { 1001 continue; 1002 } 1003 1004 this.ensureButtonContextMenu(node, aAreaNode); 1005 1006 // This needs updating in case we're resetting / undoing a reset. 1007 if (widget) { 1008 widget.currentArea = aArea; 1009 } 1010 this.insertWidgetBefore(node, currentNode, container, aArea); 1011 if (gResetting) { 1012 this.notifyListeners("onWidgetReset", node, container); 1013 } else if (gUndoResetting) { 1014 this.notifyListeners("onWidgetUndoMove", node, container); 1015 } 1016 } 1017 1018 if (currentNode) { 1019 let palette = window.gNavToolbox ? window.gNavToolbox.palette : null; 1020 let limit = currentNode.previousElementSibling; 1021 let node = container.lastElementChild; 1022 while (node && node != limit) { 1023 let previousSibling = node.previousElementSibling; 1024 // Nodes opt-in to removability. If they're removable, and we haven't 1025 // seen them in the placements array, then we toss them into the palette 1026 // if one exists. If no palette exists, we just remove the node. If the 1027 // node is not removable, we leave it where it is. However, we can only 1028 // safely touch elements that have an ID - both because we depend on 1029 // IDs (or are specials), and because such elements are not intended to 1030 // be widgets (eg, titlebar-spacer elements). 1031 if ( 1032 (node.id || this.isSpecialWidget(node)) && 1033 node.getAttribute("skipintoolbarset") != "true" 1034 ) { 1035 if (this.isWidgetRemovable(node)) { 1036 if (node.id && (gResetting || gUndoResetting)) { 1037 let widget = gPalette.get(node.id); 1038 if (widget) { 1039 widget.currentArea = null; 1040 } 1041 } 1042 if (palette && !this.isSpecialWidget(node.id)) { 1043 palette.appendChild(node); 1044 this.removeLocationAttributes(node); 1045 } else { 1046 container.removeChild(node); 1047 } 1048 } else { 1049 node.setAttribute("removable", false); 1050 log.debug( 1051 "Adding non-removable widget to placements of " + 1052 aArea + 1053 ": " + 1054 node.id 1055 ); 1056 gPlacements.get(aArea).push(node.id); 1057 gDirty = true; 1058 } 1059 } 1060 node = previousSibling; 1061 } 1062 } 1063 1064 // If there are placements in here which aren't removable from their original area, 1065 // we remove them from this area's placement array. They will (have) be(en) added 1066 // to their original area's placements array in the block above this one. 1067 if (placementsToRemove.size) { 1068 let placementAry = gPlacements.get(aArea); 1069 for (let id of placementsToRemove) { 1070 let index = placementAry.indexOf(id); 1071 placementAry.splice(index, 1); 1072 } 1073 } 1074 1075 if (gResetting) { 1076 this.notifyListeners("onAreaReset", aArea, container); 1077 } 1078 } finally { 1079 this.endBatchUpdate(); 1080 } 1081 }, 1082 1083 addPanelCloseListeners(aPanel) { 1084 gELS.addSystemEventListener(aPanel, "click", this, false); 1085 gELS.addSystemEventListener(aPanel, "keypress", this, false); 1086 let win = aPanel.ownerGlobal; 1087 if (!gPanelsForWindow.has(win)) { 1088 gPanelsForWindow.set(win, new Set()); 1089 } 1090 gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); 1091 }, 1092 1093 removePanelCloseListeners(aPanel) { 1094 gELS.removeSystemEventListener(aPanel, "click", this, false); 1095 gELS.removeSystemEventListener(aPanel, "keypress", this, false); 1096 let win = aPanel.ownerGlobal; 1097 let panels = gPanelsForWindow.get(win); 1098 if (panels) { 1099 panels.delete(this._getPanelForNode(aPanel)); 1100 } 1101 }, 1102 1103 ensureButtonContextMenu(aNode, aAreaNode, forcePanel) { 1104 const kPanelItemContextMenu = "customizationPanelItemContextMenu"; 1105 1106 let currentContextMenu = 1107 aNode.getAttribute("context") || aNode.getAttribute("contextmenu"); 1108 let contextMenuForPlace = 1109 forcePanel || "menu-panel" == CustomizableUI.getPlaceForItem(aAreaNode) 1110 ? kPanelItemContextMenu 1111 : null; 1112 if (contextMenuForPlace && !currentContextMenu) { 1113 aNode.setAttribute("context", contextMenuForPlace); 1114 } else if ( 1115 currentContextMenu == kPanelItemContextMenu && 1116 contextMenuForPlace != kPanelItemContextMenu 1117 ) { 1118 aNode.removeAttribute("context"); 1119 aNode.removeAttribute("contextmenu"); 1120 } 1121 }, 1122 1123 getWidgetProvider(aWidgetId) { 1124 if (this.isSpecialWidget(aWidgetId)) { 1125 return CustomizableUI.PROVIDER_SPECIAL; 1126 } 1127 if (gPalette.has(aWidgetId)) { 1128 return CustomizableUI.PROVIDER_API; 1129 } 1130 // If this was an API widget that was destroyed, return null: 1131 if (gSeenWidgets.has(aWidgetId)) { 1132 return null; 1133 } 1134 1135 // We fall back to the XUL provider, but we don't know for sure (at this 1136 // point) whether it exists there either. So the API is technically lying. 1137 // Ideally, it would be able to return an error value (or throw an 1138 // exception) if it really didn't exist. Our code calling this function 1139 // handles that fine, but this is a public API. 1140 return CustomizableUI.PROVIDER_XUL; 1141 }, 1142 1143 getWidgetNode(aWidgetId, aWindow) { 1144 let document = aWindow.document; 1145 1146 if (this.isSpecialWidget(aWidgetId)) { 1147 let widgetNode = 1148 document.getElementById(aWidgetId) || 1149 this.createSpecialWidget(aWidgetId, document); 1150 return [CustomizableUI.PROVIDER_SPECIAL, widgetNode]; 1151 } 1152 1153 let widget = gPalette.get(aWidgetId); 1154 if (widget) { 1155 // If we have an instance of this widget already, just use that. 1156 if (widget.instances.has(document)) { 1157 log.debug( 1158 "An instance of widget " + 1159 aWidgetId + 1160 " already exists in this " + 1161 "document. Reusing." 1162 ); 1163 return [CustomizableUI.PROVIDER_API, widget.instances.get(document)]; 1164 } 1165 1166 return [CustomizableUI.PROVIDER_API, this.buildWidget(document, widget)]; 1167 } 1168 1169 log.debug("Searching for " + aWidgetId + " in toolbox."); 1170 let node = this.findWidgetInWindow(aWidgetId, aWindow); 1171 if (node) { 1172 return [CustomizableUI.PROVIDER_XUL, node]; 1173 } 1174 1175 log.debug("No node for " + aWidgetId + " found."); 1176 return [null, null]; 1177 }, 1178 1179 registerMenuPanel(aPanelContents, aArea) { 1180 if (gBuildAreas.has(aArea) && gBuildAreas.get(aArea).has(aPanelContents)) { 1181 return; 1182 } 1183 1184 aPanelContents._customizationTarget = aPanelContents; 1185 this.addPanelCloseListeners(this._getPanelForNode(aPanelContents)); 1186 1187 let placements = gPlacements.get(aArea); 1188 this.buildArea(aArea, placements, aPanelContents); 1189 this.notifyListeners("onAreaNodeRegistered", aArea, aPanelContents); 1190 1191 for (let child of aPanelContents.children) { 1192 if (child.localName != "toolbarbutton") { 1193 if (child.localName == "toolbaritem") { 1194 this.ensureButtonContextMenu(child, aPanelContents, true); 1195 } 1196 continue; 1197 } 1198 this.ensureButtonContextMenu(child, aPanelContents, true); 1199 } 1200 1201 this.registerBuildArea(aArea, aPanelContents); 1202 }, 1203 1204 onWidgetAdded(aWidgetId, aArea, aPosition) { 1205 this.insertNode(aWidgetId, aArea, aPosition, true); 1206 1207 if (!gResetting) { 1208 this._clearPreviousUIState(); 1209 } 1210 }, 1211 1212 onWidgetRemoved(aWidgetId, aArea) { 1213 let areaNodes = gBuildAreas.get(aArea); 1214 if (!areaNodes) { 1215 return; 1216 } 1217 1218 let area = gAreas.get(aArea); 1219 let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; 1220 let isOverflowable = isToolbar && area.get("overflowable"); 1221 let showInPrivateBrowsing = gPalette.has(aWidgetId) 1222 ? gPalette.get(aWidgetId).showInPrivateBrowsing 1223 : true; 1224 1225 for (let areaNode of areaNodes) { 1226 let window = areaNode.ownerGlobal; 1227 if ( 1228 !showInPrivateBrowsing && 1229 PrivateBrowsingUtils.isWindowPrivate(window) 1230 ) { 1231 continue; 1232 } 1233 1234 let container = this.getCustomizationTarget(areaNode); 1235 let widgetNode = window.document.getElementById(aWidgetId); 1236 if (widgetNode && isOverflowable) { 1237 container = areaNode.overflowable.getContainerFor(widgetNode); 1238 } 1239 1240 if (!widgetNode || !container.contains(widgetNode)) { 1241 log.info( 1242 "Widget " + aWidgetId + " not found, unable to remove from " + aArea 1243 ); 1244 continue; 1245 } 1246 1247 this.notifyListeners( 1248 "onWidgetBeforeDOMChange", 1249 widgetNode, 1250 null, 1251 container, 1252 true 1253 ); 1254 1255 // We remove location attributes here to make sure they're gone too when a 1256 // widget is removed from a toolbar to the palette. See bug 930950. 1257 this.removeLocationAttributes(widgetNode); 1258 // We also need to remove the panel context menu if it's there: 1259 this.ensureButtonContextMenu(widgetNode); 1260 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { 1261 container.removeChild(widgetNode); 1262 } else { 1263 window.gNavToolbox.palette.appendChild(widgetNode); 1264 } 1265 this.notifyListeners( 1266 "onWidgetAfterDOMChange", 1267 widgetNode, 1268 null, 1269 container, 1270 true 1271 ); 1272 1273 let windowCache = gSingleWrapperCache.get(window); 1274 if (windowCache) { 1275 windowCache.delete(aWidgetId); 1276 } 1277 } 1278 if (!gResetting) { 1279 this._clearPreviousUIState(); 1280 } 1281 }, 1282 1283 onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) { 1284 this.insertNode(aWidgetId, aArea, aNewPosition); 1285 if (!gResetting) { 1286 this._clearPreviousUIState(); 1287 } 1288 }, 1289 1290 onCustomizeEnd(aWindow) { 1291 this._clearPreviousUIState(); 1292 }, 1293 1294 registerBuildArea(aArea, aNode) { 1295 // We ensure that the window is registered to have its customization data 1296 // cleaned up when unloading. 1297 let window = aNode.ownerGlobal; 1298 if (window.closed) { 1299 return; 1300 } 1301 this.registerBuildWindow(window); 1302 1303 // Also register this build area's toolbox. 1304 if (window.gNavToolbox) { 1305 gBuildWindows.get(window).add(window.gNavToolbox); 1306 } 1307 1308 if (!gBuildAreas.has(aArea)) { 1309 gBuildAreas.set(aArea, new Set()); 1310 } 1311 1312 gBuildAreas.get(aArea).add(aNode); 1313 1314 // Give a class to all customize targets to be used for styling in Customize Mode 1315 let customizableNode = this.getCustomizeTargetForArea(aArea, window); 1316 customizableNode.classList.add("customization-target"); 1317 }, 1318 1319 registerBuildWindow(aWindow) { 1320 if (!gBuildWindows.has(aWindow)) { 1321 gBuildWindows.set(aWindow, new Set()); 1322 1323 aWindow.addEventListener("unload", this); 1324 aWindow.addEventListener("command", this, true); 1325 1326 this.notifyListeners("onWindowOpened", aWindow); 1327 } 1328 }, 1329 1330 unregisterBuildWindow(aWindow) { 1331 aWindow.removeEventListener("unload", this); 1332 aWindow.removeEventListener("command", this, true); 1333 gPanelsForWindow.delete(aWindow); 1334 gBuildWindows.delete(aWindow); 1335 gSingleWrapperCache.delete(aWindow); 1336 let document = aWindow.document; 1337 1338 for (let [areaId, areaNodes] of gBuildAreas) { 1339 let areaProperties = gAreas.get(areaId); 1340 for (let node of areaNodes) { 1341 if (node.ownerDocument == document) { 1342 this.notifyListeners( 1343 "onAreaNodeUnregistered", 1344 areaId, 1345 this.getCustomizationTarget(node), 1346 CustomizableUI.REASON_WINDOW_CLOSED 1347 ); 1348 if (areaProperties.has("overflowable")) { 1349 node.overflowable.uninit(); 1350 node.overflowable = null; 1351 } 1352 areaNodes.delete(node); 1353 } 1354 } 1355 } 1356 1357 for (let [, widget] of gPalette) { 1358 widget.instances.delete(document); 1359 this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); 1360 } 1361 1362 for (let [, pendingNodes] of gPendingBuildAreas) { 1363 for (let i = pendingNodes.length - 1; i >= 0; i--) { 1364 if (pendingNodes[i].ownerDocument == document) { 1365 pendingNodes.splice(i, 1); 1366 } 1367 } 1368 } 1369 1370 this.notifyListeners("onWindowClosed", aWindow); 1371 }, 1372 1373 setLocationAttributes(aNode, aArea) { 1374 let props = gAreas.get(aArea); 1375 if (!props) { 1376 throw new Error( 1377 "Expected area " + 1378 aArea + 1379 " to have a properties Map " + 1380 "associated with it." 1381 ); 1382 } 1383 1384 aNode.setAttribute("cui-areatype", props.get("type") || ""); 1385 let anchor = props.get("anchor"); 1386 if (anchor) { 1387 aNode.setAttribute("cui-anchorid", anchor); 1388 } else { 1389 aNode.removeAttribute("cui-anchorid"); 1390 } 1391 }, 1392 1393 removeLocationAttributes(aNode) { 1394 aNode.removeAttribute("cui-areatype"); 1395 aNode.removeAttribute("cui-anchorid"); 1396 }, 1397 1398 insertNode(aWidgetId, aArea, aPosition, isNew) { 1399 let areaNodes = gBuildAreas.get(aArea); 1400 if (!areaNodes) { 1401 return; 1402 } 1403 1404 let placements = gPlacements.get(aArea); 1405 if (!placements) { 1406 log.error( 1407 "Could not find any placements for " + aArea + " when moving a widget." 1408 ); 1409 return; 1410 } 1411 1412 // Go through each of the nodes associated with this area and move the 1413 // widget to the requested location. 1414 for (let areaNode of areaNodes) { 1415 this.insertNodeInWindow(aWidgetId, areaNode, isNew); 1416 } 1417 }, 1418 1419 insertNodeInWindow(aWidgetId, aAreaNode, isNew) { 1420 let window = aAreaNode.ownerGlobal; 1421 let showInPrivateBrowsing = gPalette.has(aWidgetId) 1422 ? gPalette.get(aWidgetId).showInPrivateBrowsing 1423 : true; 1424 1425 if ( 1426 !showInPrivateBrowsing && 1427 PrivateBrowsingUtils.isWindowPrivate(window) 1428 ) { 1429 return; 1430 } 1431 1432 let [, widgetNode] = this.getWidgetNode(aWidgetId, window); 1433 if (!widgetNode) { 1434 log.error("Widget '" + aWidgetId + "' not found, unable to move"); 1435 return; 1436 } 1437 1438 let areaId = aAreaNode.id; 1439 if (isNew) { 1440 this.ensureButtonContextMenu(widgetNode, aAreaNode); 1441 } 1442 1443 let [insertionContainer, nextNode] = this.findInsertionPoints( 1444 widgetNode, 1445 aAreaNode 1446 ); 1447 this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); 1448 }, 1449 1450 findInsertionPoints(aNode, aAreaNode) { 1451 let areaId = aAreaNode.id; 1452 let props = gAreas.get(areaId); 1453 1454 // For overflowable toolbars, rely on them (because the work is more complicated): 1455 if ( 1456 props.get("type") == CustomizableUI.TYPE_TOOLBAR && 1457 props.get("overflowable") 1458 ) { 1459 return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); 1460 } 1461 1462 let container = this.getCustomizationTarget(aAreaNode); 1463 let placements = gPlacements.get(areaId); 1464 let nodeIndex = placements.indexOf(aNode.id); 1465 1466 while (++nodeIndex < placements.length) { 1467 let nextNodeId = placements[nodeIndex]; 1468 // We use aAreaNode here, because if aNode is in a template, its 1469 // `ownerDocument` is *not* going to be the browser.xhtml document, 1470 // so we cannot rely on it. 1471 let nextNode = aAreaNode.ownerDocument.getElementById(nextNodeId); 1472 // If the next placed widget exists, and is a direct child of the 1473 // container, or wrapped in a customize mode wrapper (toolbarpaletteitem) 1474 // inside the container, insert beside it. 1475 // We have to check the parent to avoid errors when the placement ids 1476 // are for nodes that are no longer customizable. 1477 if ( 1478 nextNode && 1479 (nextNode.parentNode == container || 1480 (nextNode.parentNode.localName == "toolbarpaletteitem" && 1481 nextNode.parentNode.parentNode == container)) 1482 ) { 1483 return [container, nextNode]; 1484 } 1485 } 1486 1487 return [container, null]; 1488 }, 1489 1490 insertWidgetBefore(aNode, aNextNode, aContainer, aArea) { 1491 this.notifyListeners( 1492 "onWidgetBeforeDOMChange", 1493 aNode, 1494 aNextNode, 1495 aContainer 1496 ); 1497 this.setLocationAttributes(aNode, aArea); 1498 aContainer.insertBefore(aNode, aNextNode); 1499 this.notifyListeners( 1500 "onWidgetAfterDOMChange", 1501 aNode, 1502 aNextNode, 1503 aContainer 1504 ); 1505 }, 1506 1507 handleEvent(aEvent) { 1508 switch (aEvent.type) { 1509 case "command": 1510 if (!this._originalEventInPanel(aEvent)) { 1511 break; 1512 } 1513 aEvent = aEvent.sourceEvent; 1514 // Fall through 1515 case "click": 1516 case "keypress": 1517 this.maybeAutoHidePanel(aEvent); 1518 break; 1519 case "unload": 1520 this.unregisterBuildWindow(aEvent.currentTarget); 1521 break; 1522 } 1523 }, 1524 1525 _originalEventInPanel(aEvent) { 1526 let e = aEvent.sourceEvent; 1527 if (!e) { 1528 return false; 1529 } 1530 let node = this._getPanelForNode(e.target); 1531 if (!node) { 1532 return false; 1533 } 1534 let win = e.view; 1535 let panels = gPanelsForWindow.get(win); 1536 return !!panels && panels.has(node); 1537 }, 1538 1539 _getSpecialIdForNode(aNode) { 1540 if (typeof aNode == "object" && aNode.localName) { 1541 if (aNode.id) { 1542 return aNode.id; 1543 } 1544 if (aNode.localName.startsWith("toolbar")) { 1545 return aNode.localName.substring(7); 1546 } 1547 return ""; 1548 } 1549 return aNode; 1550 }, 1551 1552 isSpecialWidget(aId) { 1553 aId = this._getSpecialIdForNode(aId); 1554 return ( 1555 aId.startsWith(kSpecialWidgetPfx) || 1556 aId.startsWith("separator") || 1557 aId.startsWith("spring") || 1558 aId.startsWith("spacer") 1559 ); 1560 }, 1561 1562 matchingSpecials(aId1, aId2) { 1563 aId1 = this._getSpecialIdForNode(aId1); 1564 aId2 = this._getSpecialIdForNode(aId2); 1565 1566 return ( 1567 this.isSpecialWidget(aId1) && 1568 this.isSpecialWidget(aId2) && 1569 aId1.match(/spring|spacer|separator/)[0] == 1570 aId2.match(/spring|spacer|separator/)[0] 1571 ); 1572 }, 1573 1574 ensureSpecialWidgetId(aId) { 1575 let nodeType = aId.match(/spring|spacer|separator/)[0]; 1576 // If the ID we were passed isn't a generated one, generate one now: 1577 if (nodeType == aId) { 1578 // Ids are differentiated through a unique count suffix. 1579 return kSpecialWidgetPfx + aId + ++gNewElementCount; 1580 } 1581 return aId; 1582 }, 1583 1584 createSpecialWidget(aId, aDocument) { 1585 let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; 1586 let node = aDocument.createXULElement(nodeName); 1587 node.className = "chromeclass-toolbar-additional"; 1588 node.id = this.ensureSpecialWidgetId(aId); 1589 return node; 1590 }, 1591 1592 /* Find a XUL-provided widget in a window. Don't try to use this 1593 * for an API-provided widget or a special widget. 1594 */ 1595 findWidgetInWindow(aId, aWindow) { 1596 if (!gBuildWindows.has(aWindow)) { 1597 throw new Error("Build window not registered"); 1598 } 1599 1600 if (!aId) { 1601 log.error("findWidgetInWindow was passed an empty string."); 1602 return null; 1603 } 1604 1605 let document = aWindow.document; 1606 1607 // look for a node with the same id, as the node may be 1608 // in a different toolbar. 1609 let node = document.getElementById(aId); 1610 if (node) { 1611 let parent = node.parentNode; 1612 while ( 1613 parent && 1614 !( 1615 this.getCustomizationTarget(parent) || 1616 parent == aWindow.gNavToolbox.palette 1617 ) 1618 ) { 1619 parent = parent.parentNode; 1620 } 1621 1622 if (parent) { 1623 let nodeInArea = 1624 node.parentNode.localName == "toolbarpaletteitem" 1625 ? node.parentNode 1626 : node; 1627 // Check if we're in a customization target, or in the palette: 1628 if ( 1629 (this.getCustomizationTarget(parent) == nodeInArea.parentNode && 1630 gBuildWindows.get(aWindow).has(aWindow.gNavToolbox)) || 1631 aWindow.gNavToolbox.palette == nodeInArea.parentNode 1632 ) { 1633 // Normalize the removable attribute. For backwards compat, if 1634 // the widget is not located in a toolbox palette then absence 1635 // of the "removable" attribute means it is not removable. 1636 if (!node.hasAttribute("removable")) { 1637 // If we first see this in customization mode, it may be in the 1638 // customization palette instead of the toolbox palette. 1639 node.setAttribute( 1640 "removable", 1641 !this.getCustomizationTarget(parent) 1642 ); 1643 } 1644 return node; 1645 } 1646 } 1647 } 1648 1649 let toolboxes = gBuildWindows.get(aWindow); 1650 for (let toolbox of toolboxes) { 1651 if (toolbox.palette) { 1652 // Attempt to locate an element with a matching ID within 1653 // the palette. 1654 let element = toolbox.palette.getElementsByAttribute("id", aId)[0]; 1655 if (element) { 1656 // Normalize the removable attribute. For backwards compat, this 1657 // is optional if the widget is located in the toolbox palette, 1658 // and defaults to *true*, unlike if it was located elsewhere. 1659 if (!element.hasAttribute("removable")) { 1660 element.setAttribute("removable", true); 1661 } 1662 return element; 1663 } 1664 } 1665 } 1666 return null; 1667 }, 1668 1669 buildWidget(aDocument, aWidget) { 1670 if (aDocument.documentURI != kExpectedWindowURL) { 1671 throw new Error("buildWidget was called for a non-browser window!"); 1672 } 1673 if (typeof aWidget == "string") { 1674 aWidget = gPalette.get(aWidget); 1675 } 1676 if (!aWidget) { 1677 throw new Error("buildWidget was passed a non-widget to build."); 1678 } 1679 if ( 1680 !aWidget.showInPrivateBrowsing && 1681 PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView) 1682 ) { 1683 return null; 1684 } 1685 1686 log.debug("Building " + aWidget.id + " of type " + aWidget.type); 1687 1688 let node; 1689 if (aWidget.type == "custom") { 1690 if (aWidget.onBuild) { 1691 node = aWidget.onBuild(aDocument); 1692 } 1693 if (!node || !(node instanceof aDocument.defaultView.XULElement)) { 1694 log.error( 1695 "Custom widget with id " + 1696 aWidget.id + 1697 " does not return a valid node" 1698 ); 1699 } 1700 } else { 1701 if (aWidget.onBeforeCreated) { 1702 aWidget.onBeforeCreated(aDocument); 1703 } 1704 node = aDocument.createXULElement("toolbarbutton"); 1705 1706 node.setAttribute("id", aWidget.id); 1707 node.setAttribute("widget-id", aWidget.id); 1708 node.setAttribute("widget-type", aWidget.type); 1709 if (aWidget.disabled) { 1710 node.setAttribute("disabled", true); 1711 } 1712 node.setAttribute("removable", aWidget.removable); 1713 node.setAttribute("overflows", aWidget.overflows); 1714 if (aWidget.tabSpecific) { 1715 node.setAttribute("tabspecific", aWidget.tabSpecific); 1716 } 1717 node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); 1718 let additionalTooltipArguments = []; 1719 if (aWidget.shortcutId) { 1720 let keyEl = aDocument.getElementById(aWidget.shortcutId); 1721 if (keyEl) { 1722 additionalTooltipArguments.push( 1723 ShortcutUtils.prettifyShortcut(keyEl) 1724 ); 1725 } else { 1726 log.error( 1727 "Key element with id '" + 1728 aWidget.shortcutId + 1729 "' for widget '" + 1730 aWidget.id + 1731 "' not found!" 1732 ); 1733 } 1734 } 1735 1736 let tooltip = this.getLocalizedProperty( 1737 aWidget, 1738 "tooltiptext", 1739 additionalTooltipArguments 1740 ); 1741 if (tooltip) { 1742 node.setAttribute("tooltiptext", tooltip); 1743 } 1744 1745 let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); 1746 node.addEventListener("command", commandHandler); 1747 let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); 1748 node.addEventListener("click", clickHandler); 1749 1750 let nodeClasses = ["toolbarbutton-1", "chromeclass-toolbar-additional"]; 1751 1752 // If the widget has a view, and has view showing / hiding listeners, 1753 // hook those up to this widget. 1754 if (aWidget.type == "view") { 1755 log.debug( 1756 "Widget " + 1757 aWidget.id + 1758 " has a view. Auto-registering event handlers." 1759 ); 1760 let viewNode = aDocument.getElementById(aWidget.viewId); 1761 1762 if (viewNode) { 1763 // PanelUI relies on the .PanelUI-subView class to be able to show only 1764 // one sub-view at a time. 1765 viewNode.classList.add("PanelUI-subView"); 1766 if (aWidget.source == CustomizableUI.SOURCE_BUILTIN) { 1767 nodeClasses.push("subviewbutton-nav"); 1768 } 1769 this.ensureSubviewListeners(viewNode); 1770 } else { 1771 log.error( 1772 "Could not find the view node with id: " + 1773 aWidget.viewId + 1774 ", for widget: " + 1775 aWidget.id + 1776 "." 1777 ); 1778 } 1779 1780 let keyPressHandler = this.handleWidgetKeyPress.bind( 1781 this, 1782 aWidget, 1783 node 1784 ); 1785 node.addEventListener("keypress", keyPressHandler); 1786 } 1787 node.setAttribute("class", nodeClasses.join(" ")); 1788 1789 if (aWidget.onCreated) { 1790 aWidget.onCreated(node); 1791 } 1792 } 1793 1794 aWidget.instances.set(aDocument, node); 1795 return node; 1796 }, 1797 1798 ensureSubviewListeners(viewNode) { 1799 if (viewNode._addedEventListeners) { 1800 return; 1801 } 1802 let viewId = viewNode.id; 1803 let widget = [...gPalette.values()].find(w => w.viewId == viewId); 1804 if (!widget) { 1805 return; 1806 } 1807 for (let eventName of kSubviewEvents) { 1808 let handler = "on" + eventName; 1809 if (typeof widget[handler] == "function") { 1810 viewNode.addEventListener(eventName, widget[handler]); 1811 } 1812 } 1813 viewNode._addedEventListeners = true; 1814 log.debug( 1815 "Widget " + widget.id + " showing and hiding event handlers set." 1816 ); 1817 }, 1818 1819 getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) { 1820 const kReqStringProps = ["label"]; 1821 1822 if (typeof aWidget == "string") { 1823 aWidget = gPalette.get(aWidget); 1824 } 1825 if (!aWidget) { 1826 throw new Error( 1827 "getLocalizedProperty was passed a non-widget to work with." 1828 ); 1829 } 1830 let def, name; 1831 // Let widgets pass their own string identifiers or strings, so that 1832 // we can use strings which aren't the default (in case string ids change) 1833 // and so that non-builtin-widgets can also provide labels, tooltips, etc. 1834 if (aWidget[aProp] != null) { 1835 name = aWidget[aProp]; 1836 // By using this as the default, if a widget provides a full string rather 1837 // than a string ID for localization, we will fall back to that string 1838 // and return that. 1839 def = aDef || name; 1840 } else { 1841 name = aWidget.id + "." + aProp; 1842 def = aDef || ""; 1843 } 1844 if (aWidget.localized === false) { 1845 return def; 1846 } 1847 try { 1848 if (Array.isArray(aFormatArgs) && aFormatArgs.length) { 1849 return gWidgetsBundle.formatStringFromName(name, aFormatArgs) || def; 1850 } 1851 return gWidgetsBundle.GetStringFromName(name) || def; 1852 } catch (ex) { 1853 // If an empty string was explicitly passed, treat it as an actual 1854 // value rather than a missing property. 1855 if (!def && (name != "" || kReqStringProps.includes(aProp))) { 1856 log.error("Could not localize property '" + name + "'."); 1857 } 1858 } 1859 return def; 1860 }, 1861 1862 addShortcut(aShortcutNode, aTargetNode = aShortcutNode) { 1863 // Detect if we've already been here before. 1864 if (aTargetNode.hasAttribute("shortcut")) { 1865 return; 1866 } 1867 1868 // Use ownerGlobal.document to ensure we get the right doc even for 1869 // elements in template tags. 1870 let { document } = aShortcutNode.ownerGlobal; 1871 let shortcutId = aShortcutNode.getAttribute("key"); 1872 let shortcut; 1873 if (shortcutId) { 1874 shortcut = document.getElementById(shortcutId); 1875 } else { 1876 let commandId = aShortcutNode.getAttribute("command"); 1877 if (commandId) { 1878 shortcut = ShortcutUtils.findShortcut( 1879 document.getElementById(commandId) 1880 ); 1881 } 1882 } 1883 if (!shortcut) { 1884 return; 1885 } 1886 1887 aTargetNode.setAttribute( 1888 "shortcut", 1889 ShortcutUtils.prettifyShortcut(shortcut) 1890 ); 1891 }, 1892 1893 handleWidgetCommand(aWidget, aNode, aEvent) { 1894 // Note that aEvent can be a keypress event for widgets of type "view". 1895 log.debug("handleWidgetCommand"); 1896 1897 if (aWidget.onBeforeCommand) { 1898 try { 1899 aWidget.onBeforeCommand.call(null, aEvent); 1900 } catch (e) { 1901 log.error(e); 1902 } 1903 } 1904 1905 if (aWidget.type == "button") { 1906 if (aWidget.onCommand) { 1907 try { 1908 aWidget.onCommand.call(null, aEvent); 1909 } catch (e) { 1910 log.error(e); 1911 } 1912 } else { 1913 // XXXunf Need to think this through more, and formalize. 1914 Services.obs.notifyObservers( 1915 aNode, 1916 "customizedui-widget-command", 1917 aWidget.id 1918 ); 1919 } 1920 } else if (aWidget.type == "view") { 1921 let ownerWindow = aNode.ownerGlobal; 1922 let area = this.getPlacementOfWidget(aNode.id).area; 1923 let areaType = CustomizableUI.getAreaType(area); 1924 let anchor = aNode; 1925 if (areaType != CustomizableUI.TYPE_MENU_PANEL) { 1926 let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); 1927 1928 let hasMultiView = !!aNode.closest("panelmultiview"); 1929 if (wrapper && !hasMultiView && wrapper.anchor) { 1930 this.hidePanelForNode(aNode); 1931 anchor = wrapper.anchor; 1932 } 1933 } 1934 1935 ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, aEvent); 1936 } 1937 }, 1938 1939 handleWidgetClick(aWidget, aNode, aEvent) { 1940 log.debug("handleWidgetClick"); 1941 if (aWidget.onClick) { 1942 try { 1943 aWidget.onClick.call(null, aEvent); 1944 } catch (e) { 1945 Cu.reportError(e); 1946 } 1947 } else { 1948 // XXXunf Need to think this through more, and formalize. 1949 Services.obs.notifyObservers( 1950 aNode, 1951 "customizedui-widget-click", 1952 aWidget.id 1953 ); 1954 } 1955 }, 1956 1957 handleWidgetKeyPress(aWidget, aNode, aEvent) { 1958 if (aEvent.key != " " && aEvent.key != "Enter") { 1959 return; 1960 } 1961 aEvent.stopPropagation(); 1962 aEvent.preventDefault(); 1963 this.handleWidgetCommand(aWidget, aNode, aEvent); 1964 }, 1965 1966 _getPanelForNode(aNode) { 1967 return aNode.closest("panel"); 1968 }, 1969 1970 /* 1971 * If people put things in the panel which need more than single-click interaction, 1972 * we don't want to close it. Right now we check for text inputs and menu buttons. 1973 * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank 1974 * part of the menu. 1975 */ 1976 _isOnInteractiveElement(aEvent) { 1977 function getMenuPopupForDescendant(aNode) { 1978 let lastPopup = null; 1979 while ( 1980 aNode && 1981 aNode.parentNode && 1982 aNode.parentNode.localName.startsWith("menu") 1983 ) { 1984 lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; 1985 aNode = aNode.parentNode; 1986 } 1987 return lastPopup; 1988 } 1989 1990 let target = aEvent.originalTarget; 1991 let panel = this._getPanelForNode(aEvent.currentTarget); 1992 // This can happen in e.g. customize mode. If there's no panel, 1993 // there's clearly nothing for us to close; pretend we're interactive. 1994 if (!panel) { 1995 return true; 1996 } 1997 // We keep track of: 1998 // whether we're in an input container (text field) 1999 let inInput = false; 2000 // whether we're in a popup/context menu 2001 let inMenu = false; 2002 // whether we're in a toolbarbutton/toolbaritem 2003 let inItem = false; 2004 // whether the current menuitem has a valid closemenu attribute 2005 let menuitemCloseMenu = "auto"; 2006 2007 // While keeping track of that, we go from the original target back up, 2008 // to the panel if we have to. We bail as soon as we find an input, 2009 // a toolbarbutton/item, or the panel: 2010 while (true && target) { 2011 // Skip out of iframes etc: 2012 if (target.nodeType == target.DOCUMENT_NODE) { 2013 if (!target.defaultView) { 2014 // Err, we're done. 2015 break; 2016 } 2017 // Find containing browser or iframe element in the parent doc. 2018 target = target.defaultView.docShell.chromeEventHandler; 2019 if (!target) { 2020 break; 2021 } 2022 } 2023 let tagName = target.localName; 2024 inInput = tagName == "input" || tagName == "searchbar"; 2025 inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; 2026 let isMenuItem = tagName == "menuitem"; 2027 inMenu = inMenu || isMenuItem; 2028 2029 if (isMenuItem && target.hasAttribute("closemenu")) { 2030 let closemenuVal = target.getAttribute("closemenu"); 2031 menuitemCloseMenu = 2032 closemenuVal == "single" || closemenuVal == "none" 2033 ? closemenuVal 2034 : "auto"; 2035 } 2036 // Break out of the loop immediately for disabled items, as we need to 2037 // keep the menu open in that case. 2038 if (target.getAttribute("disabled") == "true") { 2039 return true; 2040 } 2041 2042 // This isn't in the loop condition because we want to break before 2043 // changing |target| if any of these conditions are true 2044 if (inInput || inItem || target == panel) { 2045 break; 2046 } 2047 // We need specific code for popups: the item on which they were invoked 2048 // isn't necessarily in their parentNode chain: 2049 if (isMenuItem) { 2050 let topmostMenuPopup = getMenuPopupForDescendant(target); 2051 target = 2052 (topmostMenuPopup && topmostMenuPopup.triggerNode) || 2053 target.parentNode; 2054 } else { 2055 // Skip any parent shadow roots 2056 target = target.parentNode?.host?.parentNode || target.parentNode; 2057 } 2058 } 2059 2060 // If the user clicked a menu item... 2061 if (inMenu) { 2062 // We care if we're in an input also, 2063 // or if the user specified closemenu!="auto": 2064 if (inInput || menuitemCloseMenu != "auto") { 2065 return true; 2066 } 2067 // Otherwise, we're probably fine to close the panel 2068 return false; 2069 } 2070 // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, 2071 // we'll now interact with the menu 2072 if (inItem && target.getAttribute("type") == "menu") { 2073 return true; 2074 } 2075 return inInput || !inItem; 2076 }, 2077 2078 hidePanelForNode(aNode) { 2079 let panel = this._getPanelForNode(aNode); 2080 if (panel) { 2081 PanelMultiView.hidePopup(panel); 2082 } 2083 }, 2084 2085 maybeAutoHidePanel(aEvent) { 2086 if (aEvent.type == "keypress") { 2087 if (aEvent.keyCode != aEvent.DOM_VK_RETURN) { 2088 return; 2089 } 2090 // If the user hit enter/return, we don't check preventDefault - it makes sense 2091 // that this was prevented, but we probably still want to close the panel. 2092 // If consumers don't want this to happen, they should specify the closemenu 2093 // attribute. 2094 } else if (aEvent.type != "command") { 2095 // mouse events: 2096 if (aEvent.defaultPrevented || aEvent.button != 0) { 2097 return; 2098 } 2099 let isInteractive = this._isOnInteractiveElement(aEvent); 2100 log.debug("maybeAutoHidePanel: interactive ? " + isInteractive); 2101 if (isInteractive) { 2102 return; 2103 } 2104 } 2105 2106 // We can't use event.target because we might have passed an anonymous 2107 // content boundary as well, and so target points to the outer element in 2108 // that case. Unfortunately, this means we get anonymous child nodes instead 2109 // of the real ones, so looking for the 'stoooop, don't close me' attributes 2110 // is more involved. 2111 let target = aEvent.originalTarget; 2112 while (target.parentNode && target.localName != "panel") { 2113 if ( 2114 target.getAttribute("closemenu") == "none" || 2115 target.getAttribute("widget-type") == "view" 2116 ) { 2117 return; 2118 } 2119 target = target.parentNode; 2120 } 2121 2122 // If we get here, we can actually hide the popup: 2123 this.hidePanelForNode(aEvent.target); 2124 }, 2125 2126 getUnusedWidgets(aWindowPalette) { 2127 let window = aWindowPalette.ownerGlobal; 2128 let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); 2129 // We use a Set because there can be overlap between the widgets in 2130 // gPalette and the items in the palette, especially after the first 2131 // customization, since programmatically generated widgets will remain 2132 // in the toolbox palette. 2133 let widgets = new Set(); 2134 2135 // It's possible that some widgets have been defined programmatically and 2136 // have not been overlayed into the palette. We can find those inside 2137 // gPalette. 2138 for (let [id, widget] of gPalette) { 2139 if (!widget.currentArea) { 2140 if (widget.showInPrivateBrowsing || !isWindowPrivate) { 2141 widgets.add(id); 2142 } 2143 } 2144 } 2145 2146 log.debug("Iterating the actual nodes of the window palette"); 2147 for (let node of aWindowPalette.children) { 2148 log.debug("In palette children: " + node.id); 2149 if (node.id && !this.getPlacementOfWidget(node.id)) { 2150 widgets.add(node.id); 2151 } 2152 } 2153 2154 return [...widgets]; 2155 }, 2156 2157 getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas) { 2158 if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { 2159 return null; 2160 } 2161 2162 for (let [area, placements] of gPlacements) { 2163 if (!gAreas.has(area) && !aDeadAreas) { 2164 continue; 2165 } 2166 let index = placements.indexOf(aWidgetId); 2167 if (index != -1) { 2168 return { area, position: index }; 2169 } 2170 } 2171 2172 return null; 2173 }, 2174 2175 widgetExists(aWidgetId) { 2176 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { 2177 return true; 2178 } 2179 2180 // Destroyed API widgets are in gSeenWidgets, but not in gPalette: 2181 if (gSeenWidgets.has(aWidgetId)) { 2182 return false; 2183 } 2184 2185 // We're assuming XUL widgets always exist, as it's much harder to check, 2186 // and checking would be much more error prone. 2187 return true; 2188 }, 2189 2190 addWidgetToArea(aWidgetId, aArea, aPosition, aInitialAdd) { 2191 if (!gAreas.has(aArea)) { 2192 throw new Error("Unknown customization area: " + aArea); 2193 } 2194 2195 // Hack: don't want special widgets in the panel (need to check here as well 2196 // as in canWidgetMoveToArea because the menu panel is lazy): 2197 if ( 2198 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL && 2199 this.isSpecialWidget(aWidgetId) 2200 ) { 2201 return; 2202 } 2203 2204 // If this is a lazy area that hasn't been restored yet, we can't yet modify 2205 // it - would would at least like to add to it. So we keep track of it in 2206 // gFuturePlacements, and use that to add it when restoring the area. We 2207 // throw away aPosition though, as that can only be bogus if the area hasn't 2208 // yet been restorted (caller can't possibly know where its putting the 2209 // widget in relation to other widgets). 2210 if (this.isAreaLazy(aArea)) { 2211 gFuturePlacements.get(aArea).add(aWidgetId); 2212 return; 2213 } 2214 2215 if (this.isSpecialWidget(aWidgetId)) { 2216 aWidgetId = this.ensureSpecialWidgetId(aWidgetId); 2217 } 2218 2219 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); 2220 if (oldPlacement && oldPlacement.area == aArea) { 2221 this.moveWidgetWithinArea(aWidgetId, aPosition); 2222 return; 2223 } 2224 2225 // Do nothing if the widget is not allowed to move to the target area. 2226 if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { 2227 return; 2228 } 2229 2230 if (oldPlacement) { 2231 this.removeWidgetFromArea(aWidgetId); 2232 } 2233 2234 if (!gPlacements.has(aArea)) { 2235 gPlacements.set(aArea, [aWidgetId]); 2236 aPosition = 0; 2237 } else { 2238 let placements = gPlacements.get(aArea); 2239 if (typeof aPosition != "number") { 2240 aPosition = placements.length; 2241 } 2242 if (aPosition < 0) { 2243 aPosition = 0; 2244 } 2245 placements.splice(aPosition, 0, aWidgetId); 2246 } 2247 2248 let widget = gPalette.get(aWidgetId); 2249 if (widget) { 2250 widget.currentArea = aArea; 2251 widget.currentPosition = aPosition; 2252 } 2253 2254 // We initially set placements with addWidgetToArea, so in that case 2255 // we don't consider the area "dirtied". 2256 if (!aInitialAdd) { 2257 gDirtyAreaCache.add(aArea); 2258 } 2259 2260 gDirty = true; 2261 this.saveState(); 2262 2263 this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); 2264 }, 2265 2266 removeWidgetFromArea(aWidgetId) { 2267 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); 2268 if (!oldPlacement) { 2269 return; 2270 } 2271 2272 if (!this.isWidgetRemovable(aWidgetId)) { 2273 return; 2274 } 2275 2276 let placements = gPlacements.get(oldPlacement.area); 2277 let position = placements.indexOf(aWidgetId); 2278 if (position != -1) { 2279 placements.splice(position, 1); 2280 } 2281 2282 let widget = gPalette.get(aWidgetId); 2283 if (widget) { 2284 widget.currentArea = null; 2285 widget.currentPosition = null; 2286 } 2287 2288 gDirty = true; 2289 this.saveState(); 2290 gDirtyAreaCache.add(oldPlacement.area); 2291 2292 this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); 2293 }, 2294 2295 moveWidgetWithinArea(aWidgetId, aPosition) { 2296 let oldPlacement = this.getPlacementOfWidget(aWidgetId); 2297 if (!oldPlacement) { 2298 return; 2299 } 2300 2301 let placements = gPlacements.get(oldPlacement.area); 2302 if (typeof aPosition != "number") { 2303 aPosition = placements.length; 2304 } else if (aPosition < 0) { 2305 aPosition = 0; 2306 } else if (aPosition > placements.length) { 2307 aPosition = placements.length; 2308 } 2309 2310 let widget = gPalette.get(aWidgetId); 2311 if (widget) { 2312 widget.currentPosition = aPosition; 2313 widget.currentArea = oldPlacement.area; 2314 } 2315 2316 if (aPosition == oldPlacement.position) { 2317 return; 2318 } 2319 2320 placements.splice(oldPlacement.position, 1); 2321 // If we just removed the item from *before* where it is now added, 2322 // we need to compensate the position offset for that: 2323 if (oldPlacement.position < aPosition) { 2324 aPosition--; 2325 } 2326 placements.splice(aPosition, 0, aWidgetId); 2327 2328 gDirty = true; 2329 gDirtyAreaCache.add(oldPlacement.area); 2330 2331 this.saveState(); 2332 2333 this.notifyListeners( 2334 "onWidgetMoved", 2335 aWidgetId, 2336 oldPlacement.area, 2337 oldPlacement.position, 2338 aPosition 2339 ); 2340 }, 2341 2342 // Note that this does not populate gPlacements, which is done lazily. 2343 // The panel area is an exception here. 2344 loadSavedState() { 2345 let state = Services.prefs.getCharPref(kPrefCustomizationState, ""); 2346 if (!state) { 2347 log.debug("No saved state found"); 2348 // Nothing has been customized, so silently fall back to the defaults. 2349 return; 2350 } 2351 try { 2352 gSavedState = JSON.parse(state); 2353 if (typeof gSavedState != "object" || gSavedState === null) { 2354 throw new Error("Invalid saved state"); 2355 } 2356 } catch (e) { 2357 Services.prefs.clearUserPref(kPrefCustomizationState); 2358 gSavedState = {}; 2359 log.debug( 2360 "Error loading saved UI customization state, falling back to defaults." 2361 ); 2362 } 2363 2364 if (!("placements" in gSavedState)) { 2365 gSavedState.placements = {}; 2366 } 2367 2368 if (!("currentVersion" in gSavedState)) { 2369 gSavedState.currentVersion = 0; 2370 } 2371 2372 gSeenWidgets = new Set(gSavedState.seen || []); 2373 gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []); 2374 gNewElementCount = gSavedState.newElementCount || 0; 2375 }, 2376 2377 restoreStateForArea(aArea) { 2378 let placementsPreexisted = gPlacements.has(aArea); 2379 2380 this.beginBatchUpdate(); 2381 try { 2382 gRestoring = true; 2383 2384 let restored = false; 2385 if (placementsPreexisted) { 2386 log.debug("Restoring " + aArea + " from pre-existing placements"); 2387 for (let [position, id] of gPlacements.get(aArea).entries()) { 2388 this.moveWidgetWithinArea(id, position); 2389 } 2390 gDirty = false; 2391 restored = true; 2392 } else { 2393 gPlacements.set(aArea, []); 2394 } 2395 2396 if (!restored && gSavedState && aArea in gSavedState.placements) { 2397 log.debug("Restoring " + aArea + " from saved state"); 2398 let placements = gSavedState.placements[aArea]; 2399 for (let id of placements) { 2400 this.addWidgetToArea(id, aArea); 2401 } 2402 gDirty = false; 2403 restored = true; 2404 } 2405 2406 if (!restored) { 2407 log.debug("Restoring " + aArea + " from default state"); 2408 let defaults = gAreas.get(aArea).get("defaultPlacements"); 2409 if (defaults) { 2410 for (let id of defaults) { 2411 this.addWidgetToArea(id, aArea, null, true); 2412 } 2413 } 2414 gDirty = false; 2415 } 2416 2417 // Finally, add widgets to the area that were added before the it was able 2418 // to be restored. This can occur when add-ons register widgets for a 2419 // lazily-restored area before it's been restored. 2420 if (gFuturePlacements.has(aArea)) { 2421 for (let id of gFuturePlacements.get(aArea)) { 2422 this.addWidgetToArea(id, aArea); 2423 } 2424 gFuturePlacements.delete(aArea); 2425 } 2426 2427 log.debug( 2428 "Placements for " + 2429 aArea + 2430 ":\n\t" + 2431 gPlacements.get(aArea).join("\n\t") 2432 ); 2433 2434 gRestoring = false; 2435 } finally { 2436 this.endBatchUpdate(); 2437 } 2438 }, 2439 2440 saveState() { 2441 if (gInBatchStack || !gDirty) { 2442 return; 2443 } 2444 // Clone because we want to modify this map: 2445 let state = { 2446 placements: new Map(gPlacements), 2447 seen: gSeenWidgets, 2448 dirtyAreaCache: gDirtyAreaCache, 2449 currentVersion: kVersion, 2450 newElementCount: gNewElementCount, 2451 }; 2452 2453 // Merge in previously saved areas if not present in gPlacements. 2454 // This way, state is still persisted for e.g. temporarily disabled 2455 // add-ons - see bug 989338. 2456 if (gSavedState && gSavedState.placements) { 2457 for (let area of Object.keys(gSavedState.placements)) { 2458 if (!state.placements.has(area)) { 2459 let placements = gSavedState.placements[area]; 2460 state.placements.set(area, placements); 2461 } 2462 } 2463 } 2464 2465 log.debug("Saving state."); 2466 let serialized = JSON.stringify(state, this.serializerHelper); 2467 log.debug("State saved as: " + serialized); 2468 Services.prefs.setCharPref(kPrefCustomizationState, serialized); 2469 gDirty = false; 2470 }, 2471 2472 serializerHelper(aKey, aValue) { 2473 if (typeof aValue == "object" && aValue.constructor.name == "Map") { 2474 let result = {}; 2475 for (let [mapKey, mapValue] of aValue) { 2476 result[mapKey] = mapValue; 2477 } 2478 return result; 2479 } 2480 2481 if (typeof aValue == "object" && aValue.constructor.name == "Set") { 2482 return [...aValue]; 2483 } 2484 2485 return aValue; 2486 }, 2487 2488 beginBatchUpdate() { 2489 gInBatchStack++; 2490 }, 2491 2492 endBatchUpdate(aForceDirty) { 2493 gInBatchStack--; 2494 if (aForceDirty === true) { 2495 gDirty = true; 2496 } 2497 if (gInBatchStack == 0) { 2498 this.saveState(); 2499 } else if (gInBatchStack < 0) { 2500 throw new Error( 2501 "The batch editing stack should never reach a negative number." 2502 ); 2503 } 2504 }, 2505 2506 addListener(aListener) { 2507 gListeners.add(aListener); 2508 }, 2509 2510 removeListener(aListener) { 2511 if (aListener == this) { 2512 return; 2513 } 2514 2515 gListeners.delete(aListener); 2516 }, 2517 2518 notifyListeners(aEvent, ...aArgs) { 2519 if (gRestoring) { 2520 return; 2521 } 2522 2523 for (let listener of gListeners) { 2524 try { 2525 if (typeof listener[aEvent] == "function") { 2526 listener[aEvent].apply(listener, aArgs); 2527 } 2528 } catch (e) { 2529 log.error(e + " -- " + e.fileName + ":" + e.lineNumber); 2530 } 2531 } 2532 }, 2533 2534 _dispatchToolboxEventToWindow(aEventType, aDetails, aWindow) { 2535 let evt = new aWindow.CustomEvent(aEventType, { 2536 bubbles: true, 2537 cancelable: true, 2538 detail: aDetails, 2539 }); 2540 aWindow.gNavToolbox.dispatchEvent(evt); 2541 }, 2542 2543 dispatchToolboxEvent(aEventType, aDetails = {}, aWindow = null) { 2544 if (aWindow) { 2545 this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow); 2546 return; 2547 } 2548 for (let [win] of gBuildWindows) { 2549 this._dispatchToolboxEventToWindow(aEventType, aDetails, win); 2550 } 2551 }, 2552 2553 createWidget(aProperties) { 2554 let widget = this.normalizeWidget( 2555 aProperties, 2556 CustomizableUI.SOURCE_EXTERNAL 2557 ); 2558 // XXXunf This should probably throw. 2559 if (!widget) { 2560 log.error("unable to normalize widget"); 2561 return undefined; 2562 } 2563 2564 gPalette.set(widget.id, widget); 2565 2566 // Clear our caches: 2567 gGroupWrapperCache.delete(widget.id); 2568 for (let [win] of gBuildWindows) { 2569 let cache = gSingleWrapperCache.get(win); 2570 if (cache) { 2571 cache.delete(widget.id); 2572 } 2573 } 2574 2575 this.notifyListeners("onWidgetCreated", widget.id); 2576 2577 if (widget.defaultArea) { 2578 let addToDefaultPlacements = false; 2579 let area = gAreas.get(widget.defaultArea); 2580 if ( 2581 !CustomizableUI.isBuiltinToolbar(widget.defaultArea) && 2582 widget.defaultArea != CustomizableUI.AREA_FIXED_OVERFLOW_PANEL 2583 ) { 2584 addToDefaultPlacements = true; 2585 } 2586 2587 if (addToDefaultPlacements) { 2588 if (area.has("defaultPlacements")) { 2589 area.get("defaultPlacements").push(widget.id); 2590 } else { 2591 area.set("defaultPlacements", [widget.id]); 2592 } 2593 } 2594 } 2595 2596 // Look through previously saved state to see if we're restoring a widget. 2597 let seenAreas = new Set(); 2598 let widgetMightNeedAutoAdding = true; 2599 for (let [area] of gPlacements) { 2600 seenAreas.add(area); 2601 let areaIsRegistered = gAreas.has(area); 2602 let index = gPlacements.get(area).indexOf(widget.id); 2603 if (index != -1) { 2604 widgetMightNeedAutoAdding = false; 2605 if (areaIsRegistered) { 2606 widget.currentArea = area; 2607 widget.currentPosition = index; 2608 } 2609 break; 2610 } 2611 } 2612 2613 // Also look at saved state data directly in areas that haven't yet been 2614 // restored. Can't rely on this for restored areas, as they may have 2615 // changed. 2616 if (widgetMightNeedAutoAdding && gSavedState) { 2617 for (let area of Object.keys(gSavedState.placements)) { 2618 if (seenAreas.has(area)) { 2619 continue; 2620 } 2621 2622 let areaIsRegistered = gAreas.has(area); 2623 let index = gSavedState.placements[area].indexOf(widget.id); 2624 if (index != -1) { 2625 widgetMightNeedAutoAdding = false; 2626 if (areaIsRegistered) { 2627 widget.currentArea = area; 2628 widget.currentPosition = index; 2629 } 2630 break; 2631 } 2632 } 2633 } 2634 2635 // If we're restoring the widget to it's old placement, fire off the 2636 // onWidgetAdded event - our own handler will take care of adding it to 2637 // any build areas. 2638 this.beginBatchUpdate(); 2639 try { 2640 if (widget.currentArea) { 2641 this.notifyListeners( 2642 "onWidgetAdded", 2643 widget.id, 2644 widget.currentArea, 2645 widget.currentPosition 2646 ); 2647 } else if (widgetMightNeedAutoAdding) { 2648 let autoAdd = Services.prefs.getBoolPref( 2649 kPrefCustomizationAutoAdd, 2650 true 2651 ); 2652 2653 // If the widget doesn't have an existing placement, and it hasn't been 2654 // seen before, then add it to its default area so it can be used. 2655 // If the widget is not removable, we *have* to add it to its default 2656 // area here. 2657 let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id); 2658 if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) { 2659 if (widget.defaultArea) { 2660 if (this.isAreaLazy(widget.defaultArea)) { 2661 gFuturePlacements.get(widget.defaultArea).add(widget.id); 2662 } else { 2663 this.addWidgetToArea(widget.id, widget.defaultArea); 2664 } 2665 } 2666 } 2667 } 2668 } finally { 2669 // Ensure we always have this widget in gSeenWidgets, and save 2670 // state in case this needs to be done here. 2671 gSeenWidgets.add(widget.id); 2672 this.endBatchUpdate(true); 2673 } 2674 2675 this.notifyListeners( 2676 "onWidgetAfterCreation", 2677 widget.id, 2678 widget.currentArea 2679 ); 2680 return widget.id; 2681 }, 2682 2683 createBuiltinWidget(aData) { 2684 // This should only ever be called on startup, before any windows are 2685 // opened - so we know there's no build areas to handle. Also, builtin 2686 // widgets are expected to be (mostly) static, so shouldn't affect the 2687 // current placement settings. 2688 2689 // This allows a widget to be both built-in by default but also able to be 2690 // destroyed and removed from the area based on criteria that may not be 2691 // available when the widget is created -- for example, because some other 2692 // feature in the browser supersedes the widget. 2693 let conditionalDestroyPromise = aData.conditionalDestroyPromise || null; 2694 delete aData.conditionalDestroyPromise; 2695 2696 let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN); 2697 if (!widget) { 2698 log.error("Error creating builtin widget: " + aData.id); 2699 return; 2700 } 2701 2702 log.debug("Creating built-in widget with id: " + widget.id); 2703 gPalette.set(widget.id, widget); 2704 2705 if (conditionalDestroyPromise) { 2706 conditionalDestroyPromise.then( 2707 shouldDestroy => { 2708 if (shouldDestroy) { 2709 this.destroyWidget(widget.id); 2710 this.removeWidgetFromArea(widget.id); 2711 } 2712 }, 2713 err => { 2714 Cu.reportError(err); 2715 } 2716 ); 2717 } 2718 }, 2719 2720 // Returns true if the area will eventually lazily restore (but hasn't yet). 2721 isAreaLazy(aArea) { 2722 if (gPlacements.has(aArea)) { 2723 return false; 2724 } 2725 return gAreas.get(aArea).get("type") == CustomizableUI.TYPE_TOOLBAR; 2726 }, 2727 2728 // XXXunf Log some warnings here, when the data provided isn't up to scratch. 2729 normalizeWidget(aData, aSource) { 2730 let widget = { 2731 implementation: aData, 2732 source: aSource || CustomizableUI.SOURCE_EXTERNAL, 2733 instances: new Map(), 2734 currentArea: null, 2735 localized: true, 2736 removable: true, 2737 overflows: true, 2738 defaultArea: null, 2739 shortcutId: null, 2740 tabSpecific: false, 2741 tooltiptext: null, 2742 showInPrivateBrowsing: true, 2743 _introducedInVersion: -1, 2744 }; 2745 2746 if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) { 2747 log.error("Given an illegal id in normalizeWidget: " + aData.id); 2748 return null; 2749 } 2750 2751 delete widget.implementation.currentArea; 2752 widget.implementation.__defineGetter__( 2753 "currentArea", 2754 () => widget.currentArea 2755 ); 2756 2757 const kReqStringProps = ["id"]; 2758 for (let prop of kReqStringProps) { 2759 if (typeof aData[prop] != "string") { 2760 log.error( 2761 "Missing required property '" + 2762 prop + 2763 "' in normalizeWidget: " + 2764 aData.id 2765 ); 2766 return null; 2767 } 2768 widget[prop] = aData[prop]; 2769 } 2770 2771 const kOptStringProps = ["label", "tooltiptext", "shortcutId"]; 2772 for (let prop of kOptStringProps) { 2773 if (typeof aData[prop] == "string") { 2774 widget[prop] = aData[prop]; 2775 } 2776 } 2777 2778 const kOptBoolProps = [ 2779 "removable", 2780 "showInPrivateBrowsing", 2781 "overflows", 2782 "tabSpecific", 2783 "localized", 2784 ]; 2785 for (let prop of kOptBoolProps) { 2786 if (typeof aData[prop] == "boolean") { 2787 widget[prop] = aData[prop]; 2788 } 2789 } 2790 2791 // When we normalize builtin widgets, areas have not yet been registered: 2792 if ( 2793 aData.defaultArea && 2794 (aSource == CustomizableUI.SOURCE_BUILTIN || 2795 gAreas.has(aData.defaultArea)) 2796 ) { 2797 widget.defaultArea = aData.defaultArea; 2798 } else if (!widget.removable) { 2799 log.error( 2800 "Widget '" + 2801 widget.id + 2802 "' is not removable but does not specify " + 2803 "a valid defaultArea. That's not possible; it must specify a " + 2804 "valid defaultArea as well." 2805 ); 2806 return null; 2807 } 2808 2809 if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) { 2810 widget.type = aData.type; 2811 } else { 2812 widget.type = "button"; 2813 } 2814 2815 widget.disabled = aData.disabled === true; 2816 2817 if (aSource == CustomizableUI.SOURCE_BUILTIN) { 2818 widget._introducedInVersion = aData.introducedInVersion || 0; 2819 } 2820 2821 this.wrapWidgetEventHandler("onBeforeCreated", widget); 2822 this.wrapWidgetEventHandler("onClick", widget); 2823 this.wrapWidgetEventHandler("onCreated", widget); 2824 this.wrapWidgetEventHandler("onDestroyed", widget); 2825 2826 if (typeof aData.onBeforeCommand == "function") { 2827 widget.onBeforeCommand = aData.onBeforeCommand; 2828 } 2829 2830 if (widget.type == "button") { 2831 widget.onCommand = 2832 typeof aData.onCommand == "function" ? aData.onCommand : null; 2833 } else if (widget.type == "view") { 2834 if (typeof aData.viewId != "string") { 2835 log.error( 2836 "Expected a string for widget " + 2837 widget.id + 2838 " viewId, but got " + 2839 aData.viewId 2840 ); 2841 return null; 2842 } 2843 widget.viewId = aData.viewId; 2844 2845 this.wrapWidgetEventHandler("onViewShowing", widget); 2846 this.wrapWidgetEventHandler("onViewHiding", widget); 2847 } else if (widget.type == "custom") { 2848 this.wrapWidgetEventHandler("onBuild", widget); 2849 } 2850 2851 if (gPalette.has(widget.id)) { 2852 return null; 2853 } 2854 2855 return widget; 2856 }, 2857 2858 wrapWidgetEventHandler(aEventName, aWidget) { 2859 if (typeof aWidget.implementation[aEventName] != "function") { 2860 aWidget[aEventName] = null; 2861 return; 2862 } 2863 aWidget[aEventName] = function(...aArgs) { 2864 try { 2865 // Don't copy the function to the normalized widget object, instead 2866 // keep it on the original object provided to the API so that 2867 // additional methods can be implemented and used by the event 2868 // handlers. 2869 return aWidget.implementation[aEventName].apply( 2870 aWidget.implementation, 2871 aArgs 2872 ); 2873 } catch (e) { 2874 Cu.reportError(e); 2875 return undefined; 2876 } 2877 }; 2878 }, 2879 2880 destroyWidget(aWidgetId) { 2881 let widget = gPalette.get(aWidgetId); 2882 if (!widget) { 2883 gGroupWrapperCache.delete(aWidgetId); 2884 for (let [window] of gBuildWindows) { 2885 let windowCache = gSingleWrapperCache.get(window); 2886 if (windowCache) { 2887 windowCache.delete(aWidgetId); 2888 } 2889 } 2890 return; 2891 } 2892 2893 // Remove it from the default placements of an area if it was added there: 2894 if (widget.defaultArea) { 2895 let area = gAreas.get(widget.defaultArea); 2896 if (area) { 2897 let defaultPlacements = area.get("defaultPlacements"); 2898 // We can assume this is present because if a widget has a defaultArea, 2899 // we automatically create a defaultPlacements array for that area. 2900 let widgetIndex = defaultPlacements.indexOf(aWidgetId); 2901 if (widgetIndex != -1) { 2902 defaultPlacements.splice(widgetIndex, 1); 2903 } 2904 } 2905 } 2906 2907 // This will not remove the widget from gPlacements - we want to keep the 2908 // setting so the widget gets put back in it's old position if/when it 2909 // returns. 2910 for (let [window] of gBuildWindows) { 2911 let windowCache = gSingleWrapperCache.get(window); 2912 if (windowCache) { 2913 windowCache.delete(aWidgetId); 2914 } 2915 let widgetNode = 2916 window.document.getElementById(aWidgetId) || 2917 window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; 2918 if (widgetNode) { 2919 let container = widgetNode.parentNode; 2920 this.notifyListeners( 2921 "onWidgetBeforeDOMChange", 2922 widgetNode, 2923 null, 2924 container, 2925 true 2926 ); 2927 widgetNode.remove(); 2928 this.notifyListeners( 2929 "onWidgetAfterDOMChange", 2930 widgetNode, 2931 null, 2932 container, 2933 true 2934 ); 2935 } 2936 if (widget.type == "view") { 2937 let viewNode = window.document.getElementById(widget.viewId); 2938 if (viewNode) { 2939 for (let eventName of kSubviewEvents) { 2940 let handler = "on" + eventName; 2941 if (typeof widget[handler] == "function") { 2942 viewNode.removeEventListener(eventName, widget[handler]); 2943 } 2944 } 2945 } 2946 } 2947 if (widgetNode && widget.onDestroyed) { 2948 widget.onDestroyed(window.document); 2949 } 2950 } 2951 2952 gPalette.delete(aWidgetId); 2953 gGroupWrapperCache.delete(aWidgetId); 2954 2955 this.notifyListeners("onWidgetDestroyed", aWidgetId); 2956 }, 2957 2958 getCustomizeTargetForArea(aArea, aWindow) { 2959 let buildAreaNodes = gBuildAreas.get(aArea); 2960 if (!buildAreaNodes) { 2961 return null; 2962 } 2963 2964 for (let node of buildAreaNodes) { 2965 if (node.ownerGlobal == aWindow) { 2966 return this.getCustomizationTarget(node) || node; 2967 } 2968 } 2969 2970 return null; 2971 }, 2972 2973 reset() { 2974 gResetting = true; 2975 this._resetUIState(); 2976 2977 // Rebuild each registered area (across windows) to reflect the state that 2978 // was reset above. 2979 this._rebuildRegisteredAreas(); 2980 2981 for (let [widgetId, widget] of gPalette) { 2982 if (widget.source == CustomizableUI.SOURCE_EXTERNAL) { 2983 gSeenWidgets.add(widgetId); 2984 } 2985 } 2986 if (gSeenWidgets.size || gNewElementCount) { 2987 gDirty = true; 2988 this.saveState(); 2989 } 2990 2991 gResetting = false; 2992 }, 2993 2994 _resetUIState() { 2995 try { 2996 // kPrefDrawInTitlebar may not be defined on Linux/Gtk+ which throws an exception 2997 // and leads to whole test failure. Let's set a fallback default value to avoid that, 2998 // both titlebar states are tested anyway and it's not important which state is tested first. 2999 gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref( 3000 kPrefDrawInTitlebar, 3001 false 3002 ); 3003 gUIStateBeforeReset.extraDragSpace = Services.prefs.getBoolPref( 3004 kPrefExtraDragSpace 3005 ); 3006 gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref( 3007 kPrefCustomizationState 3008 ); 3009 gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity); 3010 gUIStateBeforeReset.autoTouchMode = Services.prefs.getBoolPref( 3011 kPrefAutoTouchMode 3012 ); 3013 gUIStateBeforeReset.currentTheme = gSelectedTheme; 3014 gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref( 3015 kPrefAutoHideDownloadsButton 3016 ); 3017 gUIStateBeforeReset.newElementCount = gNewElementCount; 3018 } catch (e) {} 3019 3020 Services.prefs.clearUserPref(kPrefCustomizationState); 3021 Services.prefs.clearUserPref(kPrefDrawInTitlebar); 3022 Services.prefs.clearUserPref(kPrefExtraDragSpace); 3023 Services.prefs.clearUserPref(kPrefUIDensity); 3024 Services.prefs.clearUserPref(kPrefAutoTouchMode); 3025 Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton); 3026 gDefaultTheme.enable(); 3027 gNewElementCount = 0; 3028 log.debug("State reset"); 3029 3030 // Reset placements to make restoring default placements possible. 3031 gPlacements = new Map(); 3032 gDirtyAreaCache = new Set(); 3033 gSeenWidgets = new Set(); 3034 // Clear the saved state to ensure that defaults will be used. 3035 gSavedState = null; 3036 // Restore the state for each area to its defaults 3037 for (let [areaId] of gAreas) { 3038 this.restoreStateForArea(areaId); 3039 } 3040 }, 3041 3042 _rebuildRegisteredAreas() { 3043 for (let [areaId, areaNodes] of gBuildAreas) { 3044 let placements = gPlacements.get(areaId); 3045 let isFirstChangedToolbar = true; 3046 for (let areaNode of areaNodes) { 3047 this.buildArea(areaId, placements, areaNode); 3048 3049 let area = gAreas.get(areaId); 3050 if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { 3051 let defaultCollapsed = area.get("defaultCollapsed"); 3052 let win = areaNode.ownerGlobal; 3053 if (defaultCollapsed !== null) { 3054 win.setToolbarVisibility( 3055 areaNode, 3056 !defaultCollapsed, 3057 isFirstChangedToolbar 3058 ); 3059 } 3060 } 3061 isFirstChangedToolbar = false; 3062 } 3063 } 3064 }, 3065 3066 /** 3067 * Undoes a previous reset, restoring the state of the UI to the state prior to the reset. 3068 */ 3069 undoReset() { 3070 if ( 3071 gUIStateBeforeReset.uiCustomizationState == null || 3072 gUIStateBeforeReset.drawInTitlebar == null 3073 ) { 3074 return; 3075 } 3076 gUndoResetting = true; 3077 3078 const { 3079 uiCustomizationState, 3080 drawInTitlebar, 3081 currentTheme, 3082 uiDensity, 3083 autoTouchMode, 3084 autoHideDownloadsButton, 3085 extraDragSpace, 3086 } = gUIStateBeforeReset; 3087 gNewElementCount = gUIStateBeforeReset.newElementCount; 3088 3089 // Need to clear the previous state before setting the prefs 3090 // because pref observers may check if there is a previous UI state. 3091 this._clearPreviousUIState(); 3092 3093 Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); 3094 Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar); 3095 Services.prefs.setBoolPref(kPrefExtraDragSpace, extraDragSpace); 3096 Services.prefs.setIntPref(kPrefUIDensity, uiDensity); 3097 Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode); 3098 Services.prefs.setBoolPref( 3099 kPrefAutoHideDownloadsButton, 3100 autoHideDownloadsButton 3101 ); 3102 currentTheme.enable(); 3103 this.loadSavedState(); 3104 // If the user just customizes toolbar/titlebar visibility, gSavedState will be null 3105 // and we don't need to do anything else here: 3106 if (gSavedState) { 3107 for (let areaId of Object.keys(gSavedState.placements)) { 3108 let placements = gSavedState.placements[areaId]; 3109 gPlacements.set(areaId, placements); 3110 } 3111 this._rebuildRegisteredAreas(); 3112 } 3113 3114 gUndoResetting = false; 3115 }, 3116 3117 _clearPreviousUIState() { 3118 Object.getOwnPropertyNames(gUIStateBeforeReset).forEach(prop => { 3119 gUIStateBeforeReset[prop] = null; 3120 }); 3121 }, 3122 3123 /** 3124 * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance). 3125 * @return {Boolean} whether the widget is removable 3126 */ 3127 isWidgetRemovable(aWidget) { 3128 let widgetId; 3129 let widgetNode; 3130 if (typeof aWidget == "string") { 3131 widgetId = aWidget; 3132 } else { 3133 // Skipped items could just not have ids. 3134 if (!aWidget.id && aWidget.getAttribute("skipintoolbarset") == "true") { 3135 return false; 3136 } 3137 if ( 3138 !aWidget.id && 3139 !["toolbarspring", "toolbarspacer", "toolbarseparator"].includes( 3140 aWidget.nodeName 3141 ) 3142 ) { 3143 throw new Error( 3144 "No nodes without ids that aren't special widgets should ever come into contact with CUI" 3145 ); 3146 } 3147 // Use "spring" / "spacer" / "separator" for special widgets without ids 3148 widgetId = 3149 aWidget.id || aWidget.nodeName.substring(7 /* "toolbar".length */); 3150 widgetNode = aWidget; 3151 } 3152 let provider = this.getWidgetProvider(widgetId); 3153 3154 if (provider == CustomizableUI.PROVIDER_API) { 3155 return gPalette.get(widgetId).removable; 3156 } 3157 3158 if (provider == CustomizableUI.PROVIDER_XUL) { 3159 if (gBuildWindows.size == 0) { 3160 // We don't have any build windows to look at, so just assume for now 3161 // that its removable. 3162 return true; 3163 } 3164 3165 if (!widgetNode) { 3166 // Pick any of the build windows to look at. 3167 let [window] = [...gBuildWindows][0]; 3168 [, widgetNode] = this.getWidgetNode(widgetId, window); 3169 } 3170 // If we don't have a node, we assume it's removable. This can happen because 3171 // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen 3172 // for API-provided widgets which have been destroyed. 3173 if (!widgetNode) { 3174 return true; 3175 } 3176 return widgetNode.getAttribute("removable") == "true"; 3177 } 3178 3179 // Otherwise this is either a special widget, which is always removable, or 3180 // an API widget which has already been removed from gPalette. Returning true 3181 // here allows us to then remove its ID from any placements where it might 3182 // still occur. 3183 return true; 3184 }, 3185 3186 canWidgetMoveToArea(aWidgetId, aArea) { 3187 // Special widgets can't move to the menu panel. 3188 if ( 3189 this.isSpecialWidget(aWidgetId) && 3190 gAreas.has(aArea) && 3191 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL 3192 ) { 3193 return false; 3194 } 3195 let placement = this.getPlacementOfWidget(aWidgetId); 3196 // Items in the palette can move, and items can move within their area: 3197 if (!placement || placement.area == aArea) { 3198 return true; 3199 } 3200 // For everything else, just return whether the widget is removable. 3201 return this.isWidgetRemovable(aWidgetId); 3202 }, 3203 3204 ensureWidgetPlacedInWindow(aWidgetId, aWindow) { 3205 let placement = this.getPlacementOfWidget(aWidgetId); 3206 if (!placement) { 3207 return false; 3208 } 3209 let areaNodes = gBuildAreas.get(placement.area); 3210 if (!areaNodes) { 3211 return false; 3212 } 3213 let container = [...areaNodes].filter(n => n.ownerGlobal == aWindow); 3214 if (!container.length) { 3215 return false; 3216 } 3217 let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0]; 3218 if (existingNode) { 3219 return true; 3220 } 3221 3222 this.insertNodeInWindow(aWidgetId, container[0], true); 3223 return true; 3224 }, 3225 3226 _getCurrentWidgetsInContainer(container) { 3227 // Get a list of all the widget IDs in this container, including any that 3228 // are overflown. 3229 let currentWidgets = new Set(); 3230 function addUnskippedChildren(parent) { 3231 for (let node of parent.children) { 3232 let realNode = 3233 node.localName == "toolbarpaletteitem" 3234 ? node.firstElementChild 3235 : node; 3236 if (realNode.getAttribute("skipintoolbarset") != "true") { 3237 currentWidgets.add(realNode.id); 3238 } 3239 } 3240 } 3241 addUnskippedChildren(this.getCustomizationTarget(container)); 3242 if (container.getAttribute("overflowing") == "true") { 3243 let overflowTarget = container.getAttribute("overflowtarget"); 3244 addUnskippedChildren( 3245 container.ownerDocument.getElementById(overflowTarget) 3246 ); 3247 } 3248 // Then get the sorted list of placements, and filter based on the nodes 3249 // that are present. This avoids including items that don't exist (e.g. ids 3250 // of add-on items that the user has uninstalled). 3251 let orderedPlacements = CustomizableUI.getWidgetIdsInArea(container.id); 3252 return orderedPlacements.filter(w => currentWidgets.has(w)); 3253 }, 3254 3255 get inDefaultState() { 3256 for (let [areaId, props] of gAreas) { 3257 let defaultPlacements = props.get("defaultPlacements"); 3258 let currentPlacements = gPlacements.get(areaId); 3259 // We're excluding all of the placement IDs for items that do not exist, 3260 // and items that have removable="false", 3261 // because we don't want to consider them when determining if we're 3262 // in the default state. This way, if an add-on introduces a widget 3263 // and is then uninstalled, the leftover placement doesn't cause us to 3264 // automatically assume that the buttons are not in the default state. 3265 let buildAreaNodes = gBuildAreas.get(areaId); 3266 if (buildAreaNodes && buildAreaNodes.size) { 3267 let container = [...buildAreaNodes][0]; 3268 let removableOrDefault = itemNodeOrItem => { 3269 let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem; 3270 let isRemovable = this.isWidgetRemovable(itemNodeOrItem); 3271 let isInDefault = defaultPlacements.includes(item); 3272 return isRemovable || isInDefault; 3273 }; 3274 // Toolbars need to deal with overflown widgets (if any) - so 3275 // specialcase them: 3276 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 3277 currentPlacements = this._getCurrentWidgetsInContainer( 3278 container 3279 ).filter(removableOrDefault); 3280 } else { 3281 currentPlacements = currentPlacements.filter(item => { 3282 let itemNode = container.getElementsByAttribute("id", item)[0]; 3283 return itemNode && removableOrDefault(itemNode || item); 3284 }); 3285 } 3286 3287 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 3288 let attribute = 3289 container.getAttribute("type") == "menubar" 3290 ? "autohide" 3291 : "collapsed"; 3292 let collapsed = container.getAttribute(attribute) == "true"; 3293 let defaultCollapsed = props.get("defaultCollapsed"); 3294 if (defaultCollapsed !== null && collapsed != defaultCollapsed) { 3295 log.debug( 3296 "Found " + 3297 areaId + 3298 " had non-default toolbar visibility" + 3299 "(expected " + 3300 defaultCollapsed + 3301 ", was " + 3302 collapsed + 3303 ")" 3304 ); 3305 return false; 3306 } 3307 } 3308 } 3309 log.debug( 3310 "Checking default state for " + 3311 areaId + 3312 ":\n" + 3313 currentPlacements.join(",") + 3314 "\nvs.\n" + 3315 defaultPlacements.join(",") 3316 ); 3317 3318 if (currentPlacements.length != defaultPlacements.length) { 3319 return false; 3320 } 3321 3322 for (let i = 0; i < currentPlacements.length; ++i) { 3323 if ( 3324 currentPlacements[i] != defaultPlacements[i] && 3325 !this.matchingSpecials(currentPlacements[i], defaultPlacements[i]) 3326 ) { 3327 log.debug( 3328 "Found " + 3329 currentPlacements[i] + 3330 " in " + 3331 areaId + 3332 " where " + 3333 defaultPlacements[i] + 3334 " was expected!" 3335 ); 3336 return false; 3337 } 3338 } 3339 } 3340 3341 if (Services.prefs.prefHasUserValue(kPrefUIDensity)) { 3342 log.debug(kPrefUIDensity + " pref is non-default"); 3343 return false; 3344 } 3345 3346 if (Services.prefs.prefHasUserValue(kPrefAutoTouchMode)) { 3347 log.debug(kPrefAutoTouchMode + " pref is non-default"); 3348 return false; 3349 } 3350 3351 if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) { 3352 log.debug(kPrefDrawInTitlebar + " pref is non-default"); 3353 return false; 3354 } 3355 3356 if (Services.prefs.prefHasUserValue(kPrefExtraDragSpace)) { 3357 log.debug(kPrefExtraDragSpace + " pref is non-default"); 3358 return false; 3359 } 3360 3361 // This should just be `gDefaultTheme.isActive`, but bugs... 3362 if (gDefaultTheme && gDefaultTheme.id != gSelectedTheme.id) { 3363 log.debug(gSelectedTheme.id + " theme is non-default"); 3364 return false; 3365 } 3366 3367 return true; 3368 }, 3369 3370 setToolbarVisibility(aToolbarId, aIsVisible) { 3371 // We only persist the attribute the first time. 3372 let isFirstChangedToolbar = true; 3373 for (let window of CustomizableUI.windows) { 3374 let toolbar = window.document.getElementById(aToolbarId); 3375 if (toolbar) { 3376 window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar); 3377 isFirstChangedToolbar = false; 3378 } 3379 } 3380 }, 3381}; 3382Object.freeze(CustomizableUIInternal); 3383 3384var CustomizableUI = { 3385 /** 3386 * Constant reference to the ID of the navigation toolbar. 3387 */ 3388 AREA_NAVBAR: "nav-bar", 3389 /** 3390 * Constant reference to the ID of the menubar's toolbar. 3391 */ 3392 AREA_MENUBAR: "toolbar-menubar", 3393 /** 3394 * Constant reference to the ID of the tabstrip toolbar. 3395 */ 3396 AREA_TABSTRIP: "TabsToolbar", 3397 /** 3398 * Constant reference to the ID of the bookmarks toolbar. 3399 */ 3400 AREA_BOOKMARKS: "PersonalToolbar", 3401 /** 3402 * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel. 3403 */ 3404 AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list", 3405 3406 /** 3407 * Constant indicating the area is a menu panel. 3408 */ 3409 TYPE_MENU_PANEL: "menu-panel", 3410 /** 3411 * Constant indicating the area is a toolbar. 3412 */ 3413 TYPE_TOOLBAR: "toolbar", 3414 3415 /** 3416 * Constant indicating a XUL-type provider. 3417 */ 3418 PROVIDER_XUL: "xul", 3419 /** 3420 * Constant indicating an API-type provider. 3421 */ 3422 PROVIDER_API: "api", 3423 /** 3424 * Constant indicating dynamic (special) widgets: spring, spacer, and separator. 3425 */ 3426 PROVIDER_SPECIAL: "special", 3427 3428 /** 3429 * Constant indicating the widget is built-in 3430 */ 3431 SOURCE_BUILTIN: "builtin", 3432 /** 3433 * Constant indicating the widget is externally provided 3434 * (e.g. by add-ons or other items not part of the builtin widget set). 3435 */ 3436 SOURCE_EXTERNAL: "external", 3437 3438 /** 3439 * Constant indicating the reason the event was fired was a window closing 3440 */ 3441 REASON_WINDOW_CLOSED: "window-closed", 3442 /** 3443 * Constant indicating the reason the event was fired was an area being 3444 * unregistered separately from window closing mechanics. 3445 */ 3446 REASON_AREA_UNREGISTERED: "area-unregistered", 3447 3448 /** 3449 * An iteratable property of windows managed by CustomizableUI. 3450 * Note that this can *only* be used as an iterator. ie: 3451 * for (let window of CustomizableUI.windows) { ... } 3452 */ 3453 windows: { 3454 *[Symbol.iterator]() { 3455 for (let [window] of gBuildWindows) { 3456 yield window; 3457 } 3458 }, 3459 }, 3460 3461 /** 3462 * Add a listener object that will get fired for various events regarding 3463 * customization. 3464 * 3465 * @param aListener the listener object to add 3466 * 3467 * Not all event handler methods need to be defined. 3468 * CustomizableUI will catch exceptions. Events are dispatched 3469 * synchronously on the UI thread, so if you can delay any/some of your 3470 * processing, that is advisable. The following event handlers are supported: 3471 * - onWidgetAdded(aWidgetId, aArea, aPosition) 3472 * Fired when a widget is added to an area. aWidgetId is the widget that 3473 * was added, aArea the area it was added to, and aPosition the position 3474 * in which it was added. 3475 * - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) 3476 * Fired when a widget is moved within its area. aWidgetId is the widget 3477 * that was moved, aArea the area it was moved in, aOldPosition its old 3478 * position, and aNewPosition its new position. 3479 * - onWidgetRemoved(aWidgetId, aArea) 3480 * Fired when a widget is removed from its area. aWidgetId is the widget 3481 * that was removed, aArea the area it was removed from. 3482 * 3483 * - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval) 3484 * Fired *before* a widget's DOM node is acted upon by CustomizableUI 3485 * (to add, move or remove it). aNode is the DOM node changed, aNextNode 3486 * the DOM node (if any) before which a widget will be inserted, 3487 * aContainer the *actual* DOM container (could be an overflow panel in 3488 * case of an overflowable toolbar), and aWasRemoval is true iff the 3489 * action about to happen is the removal of the DOM node. 3490 * - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) 3491 * Like onWidgetBeforeDOMChange, but fired after the change to the DOM 3492 * node of the widget. 3493 * 3494 * - onWidgetReset(aNode, aContainer) 3495 * Fired after a reset to default placements moves a widget's node to a 3496 * different location. aNode is the widget's node, aContainer is the 3497 * area it was moved into (NB: it might already have been there and been 3498 * moved to a different position!) 3499 * - onWidgetUndoMove(aNode, aContainer) 3500 * Fired after undoing a reset to default placements moves a widget's 3501 * node to a different location. aNode is the widget's node, aContainer 3502 * is the area it was moved into (NB: it might already have been there 3503 * and been moved to a different position!) 3504 * - onAreaReset(aArea, aContainer) 3505 * Fired after a reset to default placements is complete on an area's 3506 * DOM node. Note that this is fired for each DOM node. aArea is the area 3507 * that was reset, aContainer the DOM node that was reset. 3508 * 3509 * - onWidgetCreated(aWidgetId) 3510 * Fired when a widget with id aWidgetId has been created, but before it 3511 * is added to any placements or any DOM nodes have been constructed. 3512 * Only fired for API-based widgets. 3513 * - onWidgetAfterCreation(aWidgetId, aArea) 3514 * Fired after a widget with id aWidgetId has been created, and has been 3515 * added to either its default area or the area in which it was placed 3516 * previously. If the widget has no default area and/or it has never 3517 * been placed anywhere, aArea may be null. Only fired for API-based 3518 * widgets. 3519 * - onWidgetDestroyed(aWidgetId) 3520 * Fired when widgets are destroyed. aWidgetId is the widget that is 3521 * being destroyed. Only fired for API-based widgets. 3522 * - onWidgetInstanceRemoved(aWidgetId, aDocument) 3523 * Fired when a window is unloaded and a widget's instance is destroyed 3524 * because of this. Only fired for API-based widgets. 3525 * 3526 * - onWidgetDrag(aWidgetId, aArea) 3527 * Fired both when and after customize mode drag handling system tries 3528 * to determine the width and height of widget aWidgetId when dragged to a 3529 * different area. aArea will be the area the item is dragged to, or 3530 * undefined after the measurements have been done and the node has been 3531 * moved back to its 'regular' area. 3532 * 3533 * - onCustomizeStart(aWindow) 3534 * Fired when opening customize mode in aWindow. 3535 * - onCustomizeEnd(aWindow) 3536 * Fired when exiting customize mode in aWindow. 3537 * 3538 * - onWidgetOverflow(aNode, aContainer) 3539 * Fired when a widget's DOM node is overflowing its container, a toolbar, 3540 * and will be displayed in the overflow panel. 3541 * - onWidgetUnderflow(aNode, aContainer) 3542 * Fired when a widget's DOM node is *not* overflowing its container, a 3543 * toolbar, anymore. 3544 * - onWindowOpened(aWindow) 3545 * Fired when a window has been opened that is managed by CustomizableUI, 3546 * once all of the prerequisite setup has been done. 3547 * - onWindowClosed(aWindow) 3548 * Fired when a window that has been managed by CustomizableUI has been 3549 * closed. 3550 * - onAreaNodeRegistered(aArea, aContainer) 3551 * Fired after an area node is first built when it is registered. This 3552 * is often when the window has opened, but in the case of add-ons, 3553 * could fire when the node has just been registered with CustomizableUI 3554 * after an add-on update or disable/enable sequence. 3555 * - onAreaNodeUnregistered(aArea, aContainer, aReason) 3556 * Fired when an area node is explicitly unregistered by an API caller, 3557 * or by a window closing. The aReason parameter indicates which of 3558 * these is the case. 3559 */ 3560 addListener(aListener) { 3561 CustomizableUIInternal.addListener(aListener); 3562 }, 3563 /** 3564 * Remove a listener added with addListener 3565 * @param aListener the listener object to remove 3566 */ 3567 removeListener(aListener) { 3568 CustomizableUIInternal.removeListener(aListener); 3569 }, 3570 3571 /** 3572 * Register a customizable area with CustomizableUI. 3573 * @param aName the name of the area to register. Can only contain 3574 * alphanumeric characters, dashes (-) and underscores (_). 3575 * @param aProps the properties of the area. The following properties are 3576 * recognized: 3577 * - type: the type of area. Either TYPE_TOOLBAR (default) or 3578 * TYPE_MENU_PANEL; 3579 * - anchor: for a menu panel or overflowable toolbar, the 3580 * anchoring node for the panel. 3581 * - overflowable: set to true if your toolbar is overflowable. 3582 * This requires an anchor, and only has an 3583 * effect for toolbars. 3584 * - defaultPlacements: an array of widget IDs making up the 3585 * default contents of the area 3586 * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies 3587 * if toolbar is collapsed by default (default to true). 3588 * Specify null to ensure that reset/inDefaultArea don't care 3589 * about a toolbar's collapsed state 3590 */ 3591 registerArea(aName, aProperties) { 3592 CustomizableUIInternal.registerArea(aName, aProperties); 3593 }, 3594 /** 3595 * Register a concrete node for a registered area. This method needs to be called 3596 * with any toolbar in the main browser window that has its "customizable" attribute 3597 * set to true. 3598 * 3599 * Note that ideally, you should register your toolbar using registerArea 3600 * before calling this. If you don't, the node will be saved for processing when 3601 * you call registerArea. Note that CustomizableUI won't restore state in the area, 3602 * allow the user to customize it in customize mode, or otherwise deal 3603 * with it, until the area has been registered. 3604 */ 3605 registerToolbarNode(aToolbar) { 3606 CustomizableUIInternal.registerToolbarNode(aToolbar); 3607 }, 3608 /** 3609 * Register the menu panel node. This method should not be called by anyone 3610 * apart from the built-in PanelUI. 3611 * @param aPanelContents the panel contents DOM node being registered. 3612 * @param aArea the area for which to register this node. 3613 */ 3614 registerMenuPanel(aPanelContents, aArea) { 3615 CustomizableUIInternal.registerMenuPanel(aPanelContents, aArea); 3616 }, 3617 /** 3618 * Unregister a customizable area. The inverse of registerArea. 3619 * 3620 * Unregistering an area will remove all the (removable) widgets in the 3621 * area, which will return to the panel, and destroy all other traces 3622 * of the area within CustomizableUI. Note that this means the *contents* 3623 * of the area's DOM nodes will be moved to the panel or removed, but 3624 * the area's DOM nodes *themselves* will stay. 3625 * 3626 * Furthermore, by default the placements of the area will be kept in the 3627 * saved state (!) and restored if you re-register the area at a later 3628 * point. This is useful for e.g. add-ons that get disabled and then 3629 * re-enabled (e.g. when they update). 3630 * 3631 * You can override this last behaviour (and destroy the placements 3632 * information in the saved state) by passing true for aDestroyPlacements. 3633 * 3634 * @param aName the name of the area to unregister 3635 * @param aDestroyPlacements whether to destroy the placements information 3636 * for the area, too. 3637 */ 3638 unregisterArea(aName, aDestroyPlacements) { 3639 CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements); 3640 }, 3641 /** 3642 * Add a widget to an area. 3643 * If the area to which you try to add is not known to CustomizableUI, 3644 * this will throw. 3645 * If the area to which you try to add is the same as the area in which 3646 * the widget is currently placed, this will do the same as 3647 * moveWidgetWithinArea. 3648 * If the widget cannot be removed from its original location, this will 3649 * no-op. 3650 * 3651 * This will fire an onWidgetAdded notification, 3652 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification 3653 * for each window CustomizableUI knows about. 3654 * 3655 * @param aWidgetId the ID of the widget to add 3656 * @param aArea the ID of the area to add the widget to 3657 * @param aPosition the position at which to add the widget. If you do not 3658 * pass a position, the widget will be added to the end 3659 * of the area. 3660 */ 3661 addWidgetToArea(aWidgetId, aArea, aPosition) { 3662 CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition); 3663 }, 3664 /** 3665 * Remove a widget from its area. If the widget cannot be removed from its 3666 * area, or is not in any area, this will no-op. Otherwise, this will fire an 3667 * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and 3668 * onWidgetAfterDOMChange notification for each window CustomizableUI knows 3669 * about. 3670 * 3671 * @param aWidgetId the ID of the widget to remove 3672 */ 3673 removeWidgetFromArea(aWidgetId) { 3674 CustomizableUIInternal.removeWidgetFromArea(aWidgetId); 3675 }, 3676 /** 3677 * Move a widget within an area. 3678 * If the widget is not in any area, this will no-op. 3679 * If the widget is already at the indicated position, this will no-op. 3680 * 3681 * Otherwise, this will move the widget and fire an onWidgetMoved notification, 3682 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for 3683 * each window CustomizableUI knows about. 3684 * 3685 * @param aWidgetId the ID of the widget to move 3686 * @param aPosition the position to move the widget to. 3687 * Negative values or values greater than the number of 3688 * widgets will be interpreted to mean moving the widget to 3689 * respectively the first or last position. 3690 */ 3691 moveWidgetWithinArea(aWidgetId, aPosition) { 3692 CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition); 3693 }, 3694 /** 3695 * Ensure a XUL-based widget created in a window after areas were 3696 * initialized moves to its correct position. 3697 * Always prefer this over moving items in the DOM yourself. 3698 * 3699 * @param aWidgetId the ID of the widget that was just created 3700 * @param aWindow the window in which you want to ensure it was added. 3701 * 3702 * NB: why is this API per-window, you wonder? Because if you need this, 3703 * presumably you yourself need to create the widget in all the windows 3704 * and need to loop through them anyway. 3705 */ 3706 ensureWidgetPlacedInWindow(aWidgetId, aWindow) { 3707 return CustomizableUIInternal.ensureWidgetPlacedInWindow( 3708 aWidgetId, 3709 aWindow 3710 ); 3711 }, 3712 /** 3713 * Start a batch update of items. 3714 * During a batch update, the customization state is not saved to the user's 3715 * preferences file, in order to reduce (possibly sync) IO. 3716 * Calls to begin/endBatchUpdate may be nested. 3717 * 3718 * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once 3719 * for each call to beginBatchUpdate, even if there are exceptions in the 3720 * code in the batch update. Otherwise, for the duration of the 3721 * Firefox session, customization state is never saved. Typically, you 3722 * would do this using a try...finally block. 3723 */ 3724 beginBatchUpdate() { 3725 CustomizableUIInternal.beginBatchUpdate(); 3726 }, 3727 /** 3728 * End a batch update. See the documentation for beginBatchUpdate above. 3729 * 3730 * State is not saved if we believe it is identical to the last known 3731 * saved state. State is only ever saved when all batch updates have 3732 * finished (ie there has been 1 endBatchUpdate call for each 3733 * beginBatchUpdate call). If any of the endBatchUpdate calls pass 3734 * aForceDirty=true, we will flush to the prefs file. 3735 * 3736 * @param aForceDirty force CustomizableUI to flush to the prefs file when 3737 * all batch updates have finished. 3738 */ 3739 endBatchUpdate(aForceDirty) { 3740 CustomizableUIInternal.endBatchUpdate(aForceDirty); 3741 }, 3742 /** 3743 * Create a widget. 3744 * 3745 * To create a widget, you should pass an object with its desired 3746 * properties. The following properties are supported: 3747 * 3748 * - id: the ID of the widget (required). 3749 * - type: a string indicating the type of widget. Possible types 3750 * are: 3751 * 'button' - for simple button widgets (the default) 3752 * 'view' - for buttons that open a panel or subview, 3753 * depending on where they are placed. 3754 * 'custom' - for fine-grained control over the creation 3755 * of the widget. 3756 * - viewId: Only useful for views (and required there): the id of the 3757 * <panelview> that should be shown when clicking the widget. 3758 * - onBuild(aDoc): Only useful for custom widgets (and required there); a 3759 * function that will be invoked with the document in which 3760 * to build a widget. Should return the DOM node that has 3761 * been constructed. 3762 * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function 3763 * that will be invoked before the widget gets a DOM node 3764 * constructed, passing the document in which that will happen. 3765 * This is useful especially for 'view' type widgets that need 3766 * to construct their views on the fly (e.g. from bootstrapped 3767 * add-ons) 3768 * - onCreated(aNode): Attached to all widgets; a function that will be invoked 3769 * whenever the widget has a DOM node constructed, passing the 3770 * constructed node as an argument. 3771 * - onDestroyed(aDoc): Attached to all non-custom widgets; a function that 3772 * will be invoked after the widget has a DOM node destroyed, 3773 * passing the document from which it was removed. This is 3774 * useful especially for 'view' type widgets that need to 3775 * cleanup after views that were constructed on the fly. 3776 * - onBeforeCommand(aEvt): A function that will be invoked when the user 3777 * activates the button but before the command 3778 * is evaluated. Useful if code needs to run to 3779 * change the button's icon in preparation to the 3780 * pending command action. Called for both type=button 3781 * and type=view. 3782 * - onCommand(aEvt): Only useful for button widgets; a function that will be 3783 * invoked when the user activates the button. 3784 * - onClick(aEvt): Attached to all widgets; a function that will be invoked 3785 * when the user clicks the widget. 3786 * - onViewShowing(aEvt): Only useful for views; a function that will be 3787 * invoked when a user shows your view. If any event 3788 * handler calls aEvt.preventDefault(), the view will 3789 * not be shown. 3790 * 3791 * The event's `detail` property is an object with an 3792 * `addBlocker` method. Handlers which need to 3793 * perform asynchronous operations before the view is 3794 * shown may pass this method a Promise, which will 3795 * prevent the view from showing until it resolves. 3796 * Additionally, if the promise resolves to the exact 3797 * value `false`, the view will not be shown. 3798 * - onViewHiding(aEvt): Only useful for views; a function that will be 3799 * invoked when a user hides your view. 3800 * - tooltiptext: string to use for the tooltip of the widget 3801 * - label: string to use for the label of the widget 3802 * - localized: If true, or undefined, attempt to retrieve the 3803 * widget's string properties from the customizable 3804 * widgets string bundle. 3805 * - removable: whether the widget is removable (optional, default: true) 3806 * NB: if you specify false here, you must provide a 3807 * defaultArea, too. 3808 * - overflows: whether widget can overflow when in an overflowable 3809 * toolbar (optional, default: true) 3810 * - defaultArea: default area to add the widget to 3811 * (optional, default: none; required if non-removable) 3812 * - shortcutId: id of an element that has a shortcut for this widget 3813 * (optional, default: null). This is only used to display 3814 * the shortcut as part of the tooltip for builtin widgets 3815 * (which have strings inside 3816 * customizableWidgets.properties). If you're in an add-on, 3817 * you should not set this property. 3818 * - showInPrivateBrowsing: whether to show the widget in private browsing 3819 * mode (optional, default: true) 3820 * 3821 * @param aProperties the specifications for the widget. 3822 * @return a wrapper around the created widget (see getWidget) 3823 */ 3824 createWidget(aProperties) { 3825 return CustomizableUIInternal.wrapWidget( 3826 CustomizableUIInternal.createWidget(aProperties) 3827 ); 3828 }, 3829 /** 3830 * Destroy a widget 3831 * 3832 * If the widget is part of the default placements in an area, this will 3833 * remove it from there. It will also remove any DOM instances. However, 3834 * it will keep the widget in the placements for whatever area it was 3835 * in at the time. You can remove it from there yourself by calling 3836 * CustomizableUI.removeWidgetFromArea(aWidgetId). 3837 * 3838 * @param aWidgetId the ID of the widget to destroy 3839 */ 3840 destroyWidget(aWidgetId) { 3841 CustomizableUIInternal.destroyWidget(aWidgetId); 3842 }, 3843 /** 3844 * Get a wrapper object with information about the widget. 3845 * The object provides the following properties 3846 * (all read-only unless otherwise indicated): 3847 * 3848 * - id: the widget's ID; 3849 * - type: the type of widget (button, view, custom). For 3850 * XUL-provided widgets, this is always 'custom'; 3851 * - provider: the provider type of the widget, id est one of 3852 * PROVIDER_API or PROVIDER_XUL; 3853 * - forWindow(w): a method to obtain a single window wrapper for a widget, 3854 * in the window w passed as the only argument; 3855 * - instances: an array of all instances (single window wrappers) 3856 * of the widget. This array is NOT live; 3857 * - areaType: the type of the widget's current area 3858 * - isGroup: true; will be false for wrappers around single widget nodes; 3859 * - source: for API-provided widgets, whether they are built-in to 3860 * Firefox or add-on-provided; 3861 * - disabled: for API-provided widgets, whether the widget is currently 3862 * disabled. NB: this property is writable, and will toggle 3863 * all the widgets' nodes' disabled states; 3864 * - label: for API-provied widgets, the label of the widget; 3865 * - tooltiptext: for API-provided widgets, the tooltip of the widget; 3866 * - showInPrivateBrowsing: for API-provided widgets, whether the widget is 3867 * visible in private browsing; 3868 * 3869 * Single window wrappers obtained through forWindow(someWindow) or from the 3870 * instances array have the following properties 3871 * (all read-only unless otherwise indicated): 3872 * 3873 * - id: the widget's ID; 3874 * - type: the type of widget (button, view, custom). For 3875 * XUL-provided widgets, this is always 'custom'; 3876 * - provider: the provider type of the widget, id est one of 3877 * PROVIDER_API or PROVIDER_XUL; 3878 * - node: reference to the corresponding DOM node; 3879 * - anchor: the anchor on which to anchor panels opened from this 3880 * node. This will point to the overflow chevron on 3881 * overflowable toolbars if and only if your widget node 3882 * is overflowed, to the anchor for the panel menu 3883 * if your widget is inside the panel menu, and to the 3884 * node itself in all other cases; 3885 * - overflowed: boolean indicating whether the node is currently in the 3886 * overflow panel of the toolbar; 3887 * - isGroup: false; will be true for the group widget; 3888 * - label: for API-provided widgets, convenience getter for the 3889 * label attribute of the DOM node; 3890 * - tooltiptext: for API-provided widgets, convenience getter for the 3891 * tooltiptext attribute of the DOM node; 3892 * - disabled: for API-provided widgets, convenience getter *and setter* 3893 * for the disabled state of this single widget. Note that 3894 * you may prefer to use the group wrapper's getter/setter 3895 * instead. 3896 * 3897 * @param aWidgetId the ID of the widget whose information you need 3898 * @return a wrapper around the widget as described above, or null if the 3899 * widget is known not to exist (anymore). NB: non-null return 3900 * is no guarantee the widget exists because we cannot know in 3901 * advance if a XUL widget exists or not. 3902 */ 3903 getWidget(aWidgetId) { 3904 return CustomizableUIInternal.wrapWidget(aWidgetId); 3905 }, 3906 /** 3907 * Get an array of widget wrappers (see getWidget) for all the widgets 3908 * which are currently not in any area (so which are in the palette). 3909 * 3910 * @param aWindowPalette the palette (and by extension, the window) in which 3911 * CustomizableUI should look. This matters because of 3912 * course XUL-provided widgets could be available in 3913 * some windows but not others, and likewise 3914 * API-provided widgets might not exist in a private 3915 * window (because of the showInPrivateBrowsing 3916 * property). 3917 * 3918 * @return an array of widget wrappers (see getWidget) 3919 */ 3920 getUnusedWidgets(aWindowPalette) { 3921 return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map( 3922 CustomizableUIInternal.wrapWidget, 3923 CustomizableUIInternal 3924 ); 3925 }, 3926 /** 3927 * Get an array of all the widget IDs placed in an area. 3928 * Modifying the array will not affect CustomizableUI. 3929 * 3930 * @param aArea the ID of the area whose placements you want to obtain. 3931 * @return an array containing the widget IDs that are in the area. 3932 * 3933 * NB: will throw if called too early (before placements have been fetched) 3934 * or if the area is not currently known to CustomizableUI. 3935 */ 3936 getWidgetIdsInArea(aArea) { 3937 if (!gAreas.has(aArea)) { 3938 throw new Error("Unknown customization area: " + aArea); 3939 } 3940 if (!gPlacements.has(aArea)) { 3941 throw new Error("Area not yet restored"); 3942 } 3943 3944 // We need to clone this, as we don't want to let consumers muck with placements 3945 return [...gPlacements.get(aArea)]; 3946 }, 3947 /** 3948 * Get an array of widget wrappers for all the widgets in an area. This is 3949 * the same as calling getWidgetIdsInArea and .map() ing the result through 3950 * CustomizableUI.getWidget. Careful: this means that if there are IDs in there 3951 * which don't have corresponding DOM nodes, there might be nulls in this array, 3952 * or items for which wrapper.forWindow(win) will return null. 3953 * 3954 * @param aArea the ID of the area whose widgets you want to obtain. 3955 * @return an array of widget wrappers and/or null values for the widget IDs 3956 * placed in an area. 3957 * 3958 * NB: will throw if called too early (before placements have been fetched) 3959 * or if the area is not currently known to CustomizableUI. 3960 */ 3961 getWidgetsInArea(aArea) { 3962 return this.getWidgetIdsInArea(aArea).map( 3963 CustomizableUIInternal.wrapWidget, 3964 CustomizableUIInternal 3965 ); 3966 }, 3967 3968 /** 3969 * Ensure the customizable widget that matches up with this view node 3970 * will get the right subview showing/shown/hiding/hidden events when 3971 * they fire. 3972 * @param aViewNode the view node to add listeners to if they haven't 3973 * been added already. 3974 */ 3975 ensureSubviewListeners(aViewNode) { 3976 return CustomizableUIInternal.ensureSubviewListeners(aViewNode); 3977 }, 3978 /** 3979 * Obtain an array of all the area IDs known to CustomizableUI. 3980 * This array is created for you, so is modifiable without CustomizableUI 3981 * being affected. 3982 */ 3983 get areas() { 3984 return [...gAreas.keys()]; 3985 }, 3986 /** 3987 * Check what kind of area (toolbar or menu panel) an area is. This is 3988 * useful if you have a widget that needs to behave differently depending 3989 * on its location. Note that widget wrappers have a convenience getter 3990 * property (areaType) for this purpose. 3991 * 3992 * @param aArea the ID of the area whose type you want to know 3993 * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if 3994 * the area is unknown. 3995 */ 3996 getAreaType(aArea) { 3997 let area = gAreas.get(aArea); 3998 return area ? area.get("type") : null; 3999 }, 4000 /** 4001 * Check if a toolbar is collapsed by default. 4002 * 4003 * @param aArea the ID of the area whose default-collapsed state you want to know. 4004 * @return `true` or `false` depending on the area, null if the area is unknown, 4005 * or its collapsed state cannot normally be controlled by the user 4006 */ 4007 isToolbarDefaultCollapsed(aArea) { 4008 let area = gAreas.get(aArea); 4009 return area ? area.get("defaultCollapsed") : null; 4010 }, 4011 /** 4012 * Obtain the DOM node that is the customize target for an area in a 4013 * specific window. 4014 * 4015 * Areas can have a customization target that does not correspond to the 4016 * node itself. In particular, toolbars that have a customizationtarget 4017 * attribute set will have their customization target set to that node. 4018 * This means widgets will end up in the customization target, not in the 4019 * DOM node with the ID that corresponds to the area ID. This is useful 4020 * because it lets you have fixed content in a toolbar (e.g. the panel 4021 * menu item in the navbar) and have all the customizable widgets use 4022 * the customization target. 4023 * 4024 * Using this API yourself is discouraged; you should generally not need 4025 * to be asking for the DOM container node used for a particular area. 4026 * In particular, if you're wanting to check it in relation to a widget's 4027 * node, your DOM node might not be a direct child of the customize target 4028 * in a window if, for instance, the window is in customization mode, or if 4029 * this is an overflowable toolbar and the widget has been overflowed. 4030 * 4031 * @param aArea the ID of the area whose customize target you want to have 4032 * @param aWindow the window where you want to fetch the DOM node. 4033 * @return the customize target DOM node for aArea in aWindow 4034 */ 4035 getCustomizeTargetForArea(aArea, aWindow) { 4036 return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow); 4037 }, 4038 /** 4039 * Reset the customization state back to its default. 4040 * 4041 * This is the nuclear option. You should never call this except if the user 4042 * explicitly requests it. Firefox does this when the user clicks the 4043 * "Restore Defaults" button in customize mode. 4044 */ 4045 reset() { 4046 CustomizableUIInternal.reset(); 4047 }, 4048 4049 /** 4050 * Undo the previous reset, can only be called immediately after a reset. 4051 * @return a promise that will be resolved when the operation is complete. 4052 */ 4053 undoReset() { 4054 CustomizableUIInternal.undoReset(); 4055 }, 4056 4057 /** 4058 * Remove a custom toolbar added in a previous version of Firefox or using 4059 * an add-on. NB: only works on the customizable toolbars generated by 4060 * the toolbox itself. Intended for use from CustomizeMode, not by 4061 * other consumers. 4062 * @param aToolbarId the ID of the toolbar to remove 4063 */ 4064 removeExtraToolbar(aToolbarId) { 4065 CustomizableUIInternal.removeExtraToolbar(aToolbarId); 4066 }, 4067 4068 /** 4069 * Can the last Restore Defaults operation be undone. 4070 * 4071 * @return A boolean stating whether an undo of the 4072 * Restore Defaults can be performed. 4073 */ 4074 get canUndoReset() { 4075 return ( 4076 gUIStateBeforeReset.uiCustomizationState != null || 4077 gUIStateBeforeReset.drawInTitlebar != null || 4078 gUIStateBeforeReset.extraDragSpace != null || 4079 gUIStateBeforeReset.currentTheme != null || 4080 gUIStateBeforeReset.autoTouchMode != null || 4081 gUIStateBeforeReset.uiDensity != null 4082 ); 4083 }, 4084 4085 /** 4086 * Get the placement of a widget. This is by far the best way to obtain 4087 * information about what the state of your widget is. The internals of 4088 * this call are cheap (no DOM necessary) and you will know where the user 4089 * has put your widget. 4090 * 4091 * @param aWidgetId the ID of the widget whose placement you want to know 4092 * @return 4093 * { 4094 * area: "somearea", // The ID of the area where the widget is placed 4095 * position: 42 // the index in the placements array corresponding to 4096 * // your widget. 4097 * } 4098 * 4099 * OR 4100 * 4101 * null // if the widget is not placed anywhere (ie in the palette) 4102 */ 4103 getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) { 4104 return CustomizableUIInternal.getPlacementOfWidget( 4105 aWidgetId, 4106 aOnlyRegistered, 4107 aDeadAreas 4108 ); 4109 }, 4110 /** 4111 * Check if a widget can be removed from the area it's in. 4112 * 4113 * Note that if you're wanting to move the widget somewhere, you should 4114 * generally be checking canWidgetMoveToArea, because that will return 4115 * true if the widget is already in the area where you want to move it (!). 4116 * 4117 * NB: oh, also, this method might lie if the widget in question is a 4118 * XUL-provided widget and there are no windows open, because it 4119 * can obviously not check anything in this case. It will return 4120 * true. You will be able to move the widget elsewhere. However, 4121 * once the user reopens a window, the widget will move back to its 4122 * 'proper' area automagically. 4123 * 4124 * @param aWidgetId a widget ID or DOM node to check 4125 * @return true if the widget can be removed from its area, 4126 * false otherwise. 4127 */ 4128 isWidgetRemovable(aWidgetId) { 4129 return CustomizableUIInternal.isWidgetRemovable(aWidgetId); 4130 }, 4131 /** 4132 * Check if a widget can be moved to a particular area. Like 4133 * isWidgetRemovable but better, because it'll return true if the widget 4134 * is already in the right area. 4135 * 4136 * @param aWidgetId the widget ID or DOM node you want to move somewhere 4137 * @param aArea the area ID you want to move it to. 4138 * @return true if this is possible, false if it is not. The same caveats as 4139 * for isWidgetRemovable apply, however, if no windows are open. 4140 */ 4141 canWidgetMoveToArea(aWidgetId, aArea) { 4142 return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea); 4143 }, 4144 /** 4145 * Whether we're in a default state. Note that non-removable non-default 4146 * widgets and non-existing widgets are not taken into account in determining 4147 * whether we're in the default state. 4148 * 4149 * NB: this is a property with a getter. The getter is NOT cheap, because 4150 * it does smart things with non-removable non-default items, non-existent 4151 * items, and so forth. Please don't call unless necessary. 4152 */ 4153 get inDefaultState() { 4154 return CustomizableUIInternal.inDefaultState; 4155 }, 4156 4157 /** 4158 * Set a toolbar's visibility state in all windows. 4159 * @param aToolbarId the toolbar whose visibility should be adjusted 4160 * @param aIsVisible whether the toolbar should be visible 4161 */ 4162 setToolbarVisibility(aToolbarId, aIsVisible) { 4163 CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible); 4164 }, 4165 4166 /** 4167 * Get a localized property off a (widget?) object. 4168 * 4169 * NB: this is unlikely to be useful unless you're in Firefox code, because 4170 * this code uses the builtin widget stringbundle, and can't be told 4171 * to use add-on-provided strings. It's mainly here as convenience for 4172 * custom builtin widgets that build their own DOM but use the same 4173 * stringbundle as the other builtin widgets. 4174 * 4175 * @param aWidget the object whose property we should use to fetch a 4176 * localizable string; 4177 * @param aProp the property on the object to use for the fetching; 4178 * @param aFormatArgs (optional) any extra arguments to use for a formatted 4179 * string; 4180 * @param aDef (optional) the default to return if we don't find the 4181 * string in the stringbundle; 4182 * 4183 * @return the localized string, or aDef if the string isn't in the bundle. 4184 * If no default is provided, 4185 * if aProp exists on aWidget, we'll return that, 4186 * otherwise we'll return the empty string 4187 * 4188 */ 4189 getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) { 4190 return CustomizableUIInternal.getLocalizedProperty( 4191 aWidget, 4192 aProp, 4193 aFormatArgs, 4194 aDef 4195 ); 4196 }, 4197 /** 4198 * Utility function to detect, find and set a keyboard shortcut for a menuitem 4199 * or (toolbar)button. 4200 * 4201 * @param aShortcutNode the XUL node where the shortcut will be derived from; 4202 * @param aTargetNode (optional) the XUL node on which the `shortcut` 4203 * attribute will be set. If NULL, the shortcut will be 4204 * set on aShortcutNode; 4205 */ 4206 addShortcut(aShortcutNode, aTargetNode) { 4207 return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode); 4208 }, 4209 /** 4210 * Given a node, walk up to the first panel in its ancestor chain, and 4211 * close it. 4212 * 4213 * @param aNode a node whose panel should be closed; 4214 */ 4215 hidePanelForNode(aNode) { 4216 CustomizableUIInternal.hidePanelForNode(aNode); 4217 }, 4218 /** 4219 * Check if a widget is a "special" widget: a spring, spacer or separator. 4220 * 4221 * @param aWidgetId the widget ID to check. 4222 * @return true if the widget is 'special', false otherwise. 4223 */ 4224 isSpecialWidget(aWidgetId) { 4225 return CustomizableUIInternal.isSpecialWidget(aWidgetId); 4226 }, 4227 /** 4228 * Add listeners to a panel that will close it. For use from the menu panel 4229 * and overflowable toolbar implementations, unlikely to be useful for 4230 * consumers. 4231 * 4232 * @param aPanel the panel to which listeners should be attached. 4233 */ 4234 addPanelCloseListeners(aPanel) { 4235 CustomizableUIInternal.addPanelCloseListeners(aPanel); 4236 }, 4237 /** 4238 * Remove close listeners that have been added to a panel with 4239 * addPanelCloseListeners. For use from the menu panel and overflowable 4240 * toolbar implementations, unlikely to be useful for consumers. 4241 * 4242 * @param aPanel the panel from which listeners should be removed. 4243 */ 4244 removePanelCloseListeners(aPanel) { 4245 CustomizableUIInternal.removePanelCloseListeners(aPanel); 4246 }, 4247 /** 4248 * Notify listeners a widget is about to be dragged to an area. For use from 4249 * Customize Mode only, do not use otherwise. 4250 * 4251 * @param aWidgetId the ID of the widget that is being dragged to an area. 4252 * @param aArea the ID of the area to which the widget is being dragged. 4253 */ 4254 onWidgetDrag(aWidgetId, aArea) { 4255 CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea); 4256 }, 4257 /** 4258 * Notify listeners that a window is entering customize mode. For use from 4259 * Customize Mode only, do not use otherwise. 4260 * @param aWindow the window entering customize mode 4261 */ 4262 notifyStartCustomizing(aWindow) { 4263 CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow); 4264 }, 4265 /** 4266 * Notify listeners that a window is exiting customize mode. For use from 4267 * Customize Mode only, do not use otherwise. 4268 * @param aWindow the window exiting customize mode 4269 */ 4270 notifyEndCustomizing(aWindow) { 4271 CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow); 4272 }, 4273 4274 /** 4275 * Notify toolbox(es) of a particular event. If you don't pass aWindow, 4276 * all toolboxes will be notified. For use from Customize Mode only, 4277 * do not use otherwise. 4278 * @param aEvent the name of the event to send. 4279 * @param aDetails optional, the details of the event. 4280 * @param aWindow optional, the window in which to send the event. 4281 */ 4282 dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) { 4283 CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow); 4284 }, 4285 4286 /** 4287 * Check whether an area is overflowable. 4288 * 4289 * @param aAreaId the ID of an area to check for overflowable-ness 4290 * @return true if the area is overflowable, false otherwise. 4291 */ 4292 isAreaOverflowable(aAreaId) { 4293 let area = gAreas.get(aAreaId); 4294 return area 4295 ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable") 4296 : false; 4297 }, 4298 /** 4299 * Obtain a string indicating the place of an element. This is intended 4300 * for use from customize mode; You should generally use getPlacementOfWidget 4301 * instead, which is cheaper because it does not use the DOM. 4302 * 4303 * @param aElement the DOM node whose place we need to check 4304 * @return "toolbar" if the node is in a toolbar, "panel" if it is in the 4305 * menu panel, "palette" if it is in the (visible!) customization 4306 * palette, undefined otherwise. 4307 */ 4308 getPlaceForItem(aElement) { 4309 let place; 4310 let node = aElement; 4311 while (node && !place) { 4312 if (node.localName == "toolbar") { 4313 place = "toolbar"; 4314 } else if (node.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { 4315 place = "menu-panel"; 4316 } else if (node.id == "customization-palette") { 4317 place = "palette"; 4318 } 4319 4320 node = node.parentNode; 4321 } 4322 return place; 4323 }, 4324 4325 /** 4326 * Check if a toolbar is builtin or not. 4327 * @param aToolbarId the ID of the toolbar you want to check 4328 */ 4329 isBuiltinToolbar(aToolbarId) { 4330 return CustomizableUIInternal._builtinToolbars.has(aToolbarId); 4331 }, 4332 4333 /** 4334 * Create an instance of a spring, spacer or separator. 4335 * @param aId the type of special widget (spring, spacer or separator) 4336 * @param aDocument the document in which to create it. 4337 */ 4338 createSpecialWidget(aId, aDocument) { 4339 return CustomizableUIInternal.createSpecialWidget(aId, aDocument); 4340 }, 4341 4342 /** 4343 * Fills a submenu with menu items. 4344 * @param aMenuItems the menu items to display. 4345 * @param aSubview the subview to fill. 4346 */ 4347 fillSubviewFromMenuItems(aMenuItems, aSubview) { 4348 let attrs = [ 4349 "oncommand", 4350 "onclick", 4351 "label", 4352 "key", 4353 "disabled", 4354 "command", 4355 "observes", 4356 "hidden", 4357 "class", 4358 "origin", 4359 "image", 4360 "checked", 4361 "style", 4362 ]; 4363 4364 // Use ownerGlobal.document to ensure we get the right doc even for 4365 // elements in template tags. 4366 let doc = aSubview.ownerGlobal.document; 4367 let fragment = doc.createDocumentFragment(); 4368 for (let menuChild of aMenuItems) { 4369 if (menuChild.hidden) { 4370 continue; 4371 } 4372 4373 let subviewItem; 4374 if (menuChild.localName == "menuseparator") { 4375 // Don't insert duplicate or leading separators. This can happen if there are 4376 // menus (which we don't copy) above the separator. 4377 if ( 4378 !fragment.lastElementChild || 4379 fragment.lastElementChild.localName == "toolbarseparator" 4380 ) { 4381 continue; 4382 } 4383 subviewItem = doc.createXULElement("toolbarseparator"); 4384 } else if (menuChild.localName == "menuitem") { 4385 subviewItem = doc.createXULElement("toolbarbutton"); 4386 CustomizableUI.addShortcut(menuChild, subviewItem); 4387 4388 let item = menuChild; 4389 if (!item.hasAttribute("onclick")) { 4390 subviewItem.addEventListener("click", event => { 4391 let newEvent = new doc.defaultView.MouseEvent(event.type, event); 4392 4393 // Telemetry should only pay attention to the original event. 4394 BrowserUsageTelemetry.ignoreEvent(newEvent); 4395 item.dispatchEvent(newEvent); 4396 }); 4397 } 4398 4399 if (!item.hasAttribute("oncommand")) { 4400 subviewItem.addEventListener("command", event => { 4401 let newEvent = doc.createEvent("XULCommandEvent"); 4402 newEvent.initCommandEvent( 4403 event.type, 4404 event.bubbles, 4405 event.cancelable, 4406 event.view, 4407 event.detail, 4408 event.ctrlKey, 4409 event.altKey, 4410 event.shiftKey, 4411 event.metaKey, 4412 event.sourceEvent, 4413 0 4414 ); 4415 4416 // Telemetry should only pay attention to the original event. 4417 BrowserUsageTelemetry.ignoreEvent(newEvent); 4418 item.dispatchEvent(newEvent); 4419 }); 4420 } 4421 } else { 4422 continue; 4423 } 4424 for (let attr of attrs) { 4425 let attrVal = menuChild.getAttribute(attr); 4426 if (attrVal) { 4427 subviewItem.setAttribute(attr, attrVal); 4428 } 4429 } 4430 // We do this after so the .subviewbutton class doesn't get overriden. 4431 if (menuChild.localName == "menuitem") { 4432 subviewItem.classList.add("subviewbutton"); 4433 } 4434 fragment.appendChild(subviewItem); 4435 } 4436 aSubview.appendChild(fragment); 4437 }, 4438 4439 /** 4440 * A helper function for clearing subviews. 4441 * @param aSubview the subview to clear. 4442 */ 4443 clearSubview(aSubview) { 4444 let parent = aSubview.parentNode; 4445 // We'll take the container out of the document before cleaning it out 4446 // to avoid reflowing each time we remove something. 4447 parent.removeChild(aSubview); 4448 4449 while (aSubview.firstChild) { 4450 aSubview.firstChild.remove(); 4451 } 4452 4453 parent.appendChild(aSubview); 4454 }, 4455 4456 getCustomizationTarget(aElement) { 4457 return CustomizableUIInternal.getCustomizationTarget(aElement); 4458 }, 4459}; 4460Object.freeze(CustomizableUI); 4461Object.freeze(CustomizableUI.windows); 4462 4463/** 4464 * All external consumers of widgets are really interacting with these wrappers 4465 * which provide a common interface. 4466 */ 4467 4468/** 4469 * WidgetGroupWrapper is the common interface for interacting with an entire 4470 * widget group - AKA, all instances of a widget across a series of windows. 4471 * This particular wrapper is only used for widgets created via the provider 4472 * API. 4473 */ 4474function WidgetGroupWrapper(aWidget) { 4475 this.isGroup = true; 4476 4477 const kBareProps = [ 4478 "id", 4479 "source", 4480 "type", 4481 "disabled", 4482 "label", 4483 "tooltiptext", 4484 "showInPrivateBrowsing", 4485 "viewId", 4486 ]; 4487 for (let prop of kBareProps) { 4488 let propertyName = prop; 4489 this.__defineGetter__(propertyName, () => aWidget[propertyName]); 4490 } 4491 4492 this.__defineGetter__("provider", () => CustomizableUI.PROVIDER_API); 4493 4494 this.__defineSetter__("disabled", function(aValue) { 4495 aValue = !!aValue; 4496 aWidget.disabled = aValue; 4497 for (let [, instance] of aWidget.instances) { 4498 instance.disabled = aValue; 4499 } 4500 }); 4501 4502 this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) { 4503 let wrapperMap; 4504 if (!gSingleWrapperCache.has(aWindow)) { 4505 wrapperMap = new Map(); 4506 gSingleWrapperCache.set(aWindow, wrapperMap); 4507 } else { 4508 wrapperMap = gSingleWrapperCache.get(aWindow); 4509 } 4510 if (wrapperMap.has(aWidget.id)) { 4511 return wrapperMap.get(aWidget.id); 4512 } 4513 4514 let instance = aWidget.instances.get(aWindow.document); 4515 if (!instance) { 4516 instance = CustomizableUIInternal.buildWidget(aWindow.document, aWidget); 4517 } 4518 4519 let wrapper = new WidgetSingleWrapper(aWidget, instance); 4520 wrapperMap.set(aWidget.id, wrapper); 4521 return wrapper; 4522 }; 4523 4524 this.__defineGetter__("instances", function() { 4525 // Can't use gBuildWindows here because some areas load lazily: 4526 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); 4527 if (!placement) { 4528 return []; 4529 } 4530 let area = placement.area; 4531 let buildAreas = gBuildAreas.get(area); 4532 if (!buildAreas) { 4533 return []; 4534 } 4535 return Array.from(buildAreas, node => this.forWindow(node.ownerGlobal)); 4536 }); 4537 4538 this.__defineGetter__("areaType", function() { 4539 let areaProps = gAreas.get(aWidget.currentArea); 4540 return areaProps && areaProps.get("type"); 4541 }); 4542 4543 Object.freeze(this); 4544} 4545 4546/** 4547 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in 4548 * a particular window. 4549 */ 4550function WidgetSingleWrapper(aWidget, aNode) { 4551 this.isGroup = false; 4552 4553 this.node = aNode; 4554 this.provider = CustomizableUI.PROVIDER_API; 4555 4556 const kGlobalProps = ["id", "type"]; 4557 for (let prop of kGlobalProps) { 4558 this[prop] = aWidget[prop]; 4559 } 4560 4561 const kNodeProps = ["label", "tooltiptext"]; 4562 for (let prop of kNodeProps) { 4563 let propertyName = prop; 4564 // Look at the node for these, instead of the widget data, to ensure the 4565 // wrapper always reflects this live instance. 4566 this.__defineGetter__(propertyName, () => aNode.getAttribute(propertyName)); 4567 } 4568 4569 this.__defineGetter__("disabled", () => aNode.disabled); 4570 this.__defineSetter__("disabled", function(aValue) { 4571 aNode.disabled = !!aValue; 4572 }); 4573 4574 this.__defineGetter__("anchor", function() { 4575 let anchorId; 4576 // First check for an anchor for the area: 4577 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); 4578 if (placement) { 4579 anchorId = gAreas.get(placement.area).get("anchor"); 4580 } 4581 if (!anchorId) { 4582 anchorId = aNode.getAttribute("cui-anchorid"); 4583 } 4584 4585 return anchorId ? aNode.ownerDocument.getElementById(anchorId) : aNode; 4586 }); 4587 4588 this.__defineGetter__("overflowed", function() { 4589 return aNode.getAttribute("overflowedItem") == "true"; 4590 }); 4591 4592 Object.freeze(this); 4593} 4594 4595/** 4596 * XULWidgetGroupWrapper is the common interface for interacting with an entire 4597 * widget group - AKA, all instances of a widget across a series of windows. 4598 * This particular wrapper is only used for widgets created via the old-school 4599 * XUL method (overlays, or programmatically injecting toolbaritems, or other 4600 * such things). 4601 */ 4602// XXXunf Going to need to hook this up to some events to keep it all live. 4603function XULWidgetGroupWrapper(aWidgetId) { 4604 this.isGroup = true; 4605 this.id = aWidgetId; 4606 this.type = "custom"; 4607 this.provider = CustomizableUI.PROVIDER_XUL; 4608 4609 this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) { 4610 let wrapperMap; 4611 if (!gSingleWrapperCache.has(aWindow)) { 4612 wrapperMap = new Map(); 4613 gSingleWrapperCache.set(aWindow, wrapperMap); 4614 } else { 4615 wrapperMap = gSingleWrapperCache.get(aWindow); 4616 } 4617 if (wrapperMap.has(aWidgetId)) { 4618 return wrapperMap.get(aWidgetId); 4619 } 4620 4621 let instance = aWindow.document.getElementById(aWidgetId); 4622 if (!instance) { 4623 // Toolbar palettes aren't part of the document, so elements in there 4624 // won't be found via document.getElementById(). 4625 instance = aWindow.gNavToolbox.palette.getElementsByAttribute( 4626 "id", 4627 aWidgetId 4628 )[0]; 4629 } 4630 4631 let wrapper = new XULWidgetSingleWrapper( 4632 aWidgetId, 4633 instance, 4634 aWindow.document 4635 ); 4636 wrapperMap.set(aWidgetId, wrapper); 4637 return wrapper; 4638 }; 4639 4640 this.__defineGetter__("areaType", function() { 4641 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); 4642 if (!placement) { 4643 return null; 4644 } 4645 4646 let areaProps = gAreas.get(placement.area); 4647 return areaProps && areaProps.get("type"); 4648 }); 4649 4650 this.__defineGetter__("instances", function() { 4651 return Array.from(gBuildWindows, wins => this.forWindow(wins[0])); 4652 }); 4653 4654 Object.freeze(this); 4655} 4656 4657/** 4658 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL 4659 * widget in a particular window. 4660 */ 4661function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) { 4662 this.isGroup = false; 4663 4664 this.id = aWidgetId; 4665 this.type = "custom"; 4666 this.provider = CustomizableUI.PROVIDER_XUL; 4667 4668 let weakDoc = Cu.getWeakReference(aDocument); 4669 // If we keep a strong ref, the weak ref will never die, so null it out: 4670 aDocument = null; 4671 4672 this.__defineGetter__("node", function() { 4673 // If we've set this to null (further down), we're sure there's nothing to 4674 // be gotten here, so bail out early: 4675 if (!weakDoc) { 4676 return null; 4677 } 4678 if (aNode) { 4679 // Return the last known node if it's still in the DOM... 4680 if (aNode.isConnected) { 4681 return aNode; 4682 } 4683 // ... or the toolbox 4684 let toolbox = aNode.ownerGlobal.gNavToolbox; 4685 if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) { 4686 return aNode; 4687 } 4688 // If it isn't, clear the cached value and fall through to the "slow" case: 4689 aNode = null; 4690 } 4691 4692 let doc = weakDoc.get(); 4693 if (doc) { 4694 // Store locally so we can cache the result: 4695 aNode = CustomizableUIInternal.findWidgetInWindow( 4696 aWidgetId, 4697 doc.defaultView 4698 ); 4699 return aNode; 4700 } 4701 // The weakref to the document is dead, we're done here forever more: 4702 weakDoc = null; 4703 return null; 4704 }); 4705 4706 this.__defineGetter__("anchor", function() { 4707 let anchorId; 4708 // First check for an anchor for the area: 4709 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); 4710 if (placement) { 4711 anchorId = gAreas.get(placement.area).get("anchor"); 4712 } 4713 4714 let node = this.node; 4715 if (!anchorId && node) { 4716 anchorId = node.getAttribute("cui-anchorid"); 4717 } 4718 4719 return anchorId && node 4720 ? node.ownerDocument.getElementById(anchorId) 4721 : node; 4722 }); 4723 4724 this.__defineGetter__("overflowed", function() { 4725 let node = this.node; 4726 if (!node) { 4727 return false; 4728 } 4729 return node.getAttribute("overflowedItem") == "true"; 4730 }); 4731 4732 Object.freeze(this); 4733} 4734 4735const OVERFLOW_PANEL_HIDE_DELAY_MS = 500; 4736 4737function OverflowableToolbar(aToolbarNode) { 4738 this._toolbar = aToolbarNode; 4739 this._target = CustomizableUI.getCustomizationTarget(this._toolbar); 4740 if (this._target.parentNode != this._toolbar) { 4741 throw new Error( 4742 "Customization target must be a direct child of an overflowable toolbar." 4743 ); 4744 } 4745 this._collapsed = new Map(); 4746 this._enabled = true; 4747 4748 this._toolbar.setAttribute("overflowable", "true"); 4749 let doc = this._toolbar.ownerDocument; 4750 this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget")); 4751 this._list._customizationTarget = this._list; 4752 4753 let window = this._toolbar.ownerGlobal; 4754 if (window.gBrowserInit.delayedStartupFinished) { 4755 this.init(); 4756 } else { 4757 Services.obs.addObserver(this, "browser-delayed-startup-finished"); 4758 } 4759} 4760 4761OverflowableToolbar.prototype = { 4762 initialized: false, 4763 4764 observe(aSubject, aTopic, aData) { 4765 if ( 4766 aTopic == "browser-delayed-startup-finished" && 4767 aSubject == this._toolbar.ownerGlobal 4768 ) { 4769 Services.obs.removeObserver(this, "browser-delayed-startup-finished"); 4770 this.init(); 4771 } 4772 }, 4773 4774 init() { 4775 let doc = this._toolbar.ownerDocument; 4776 let window = doc.defaultView; 4777 window.addEventListener("resize", this); 4778 window.gNavToolbox.addEventListener("customizationstarting", this); 4779 window.gNavToolbox.addEventListener("aftercustomization", this); 4780 4781 let chevronId = this._toolbar.getAttribute("overflowbutton"); 4782 this._chevron = doc.getElementById(chevronId); 4783 this._chevron.addEventListener("mousedown", this); 4784 this._chevron.addEventListener("keypress", this); 4785 this._chevron.addEventListener("dragover", this); 4786 this._chevron.addEventListener("dragend", this); 4787 4788 let panelId = this._toolbar.getAttribute("overflowpanel"); 4789 this._panel = doc.getElementById(panelId); 4790 this._panel.addEventListener("popuphiding", this); 4791 CustomizableUIInternal.addPanelCloseListeners(this._panel); 4792 4793 CustomizableUI.addListener(this); 4794 4795 this._checkOverflow(); 4796 4797 this.initialized = true; 4798 }, 4799 4800 uninit() { 4801 this._toolbar.removeAttribute("overflowable"); 4802 4803 if (!this.initialized) { 4804 Services.obs.removeObserver(this, "browser-delayed-startup-finished"); 4805 return; 4806 } 4807 4808 this._disable(); 4809 4810 let window = this._toolbar.ownerGlobal; 4811 window.removeEventListener("resize", this); 4812 window.gNavToolbox.removeEventListener("customizationstarting", this); 4813 window.gNavToolbox.removeEventListener("aftercustomization", this); 4814 this._chevron.removeEventListener("mousedown", this); 4815 this._chevron.removeEventListener("keypress", this); 4816 this._chevron.removeEventListener("dragover", this); 4817 this._chevron.removeEventListener("dragend", this); 4818 this._panel.removeEventListener("popuphiding", this); 4819 CustomizableUI.removeListener(this); 4820 CustomizableUIInternal.removePanelCloseListeners(this._panel); 4821 }, 4822 4823 handleEvent(aEvent) { 4824 switch (aEvent.type) { 4825 case "aftercustomization": 4826 this._enable(); 4827 break; 4828 case "mousedown": 4829 if (aEvent.button != 0) { 4830 break; 4831 } 4832 if (aEvent.target == this._chevron) { 4833 this._onClickChevron(aEvent); 4834 } else { 4835 PanelMultiView.hidePopup(this._panel); 4836 } 4837 break; 4838 case "keypress": 4839 if ( 4840 aEvent.target == this._chevron && 4841 (aEvent.key == " " || aEvent.key == "Enter") 4842 ) { 4843 this._onClickChevron(aEvent); 4844 } 4845 break; 4846 case "customizationstarting": 4847 this._disable(); 4848 break; 4849 case "dragover": 4850 if (this._enabled) { 4851 this._showWithTimeout(); 4852 } 4853 break; 4854 case "dragend": 4855 PanelMultiView.hidePopup(this._panel); 4856 break; 4857 case "popuphiding": 4858 this._onPanelHiding(aEvent); 4859 break; 4860 case "resize": 4861 this._onResize(aEvent); 4862 } 4863 }, 4864 4865 show(aEvent) { 4866 if (this._panel.state == "open") { 4867 return Promise.resolve(); 4868 } 4869 return new Promise(resolve => { 4870 let doc = this._panel.ownerDocument; 4871 this._panel.hidden = false; 4872 let multiview = this._panel.querySelector("panelmultiview"); 4873 let mainViewId = multiview.getAttribute("mainViewId"); 4874 let mainView = doc.getElementById(mainViewId); 4875 let contextMenu = doc.getElementById(mainView.getAttribute("context")); 4876 gELS.addSystemEventListener(contextMenu, "command", this, true); 4877 let anchor = this._chevron.icon; 4878 4879 let popupshown = false; 4880 this._panel.addEventListener( 4881 "popupshown", 4882 () => { 4883 popupshown = true; 4884 this._panel.addEventListener("dragover", this); 4885 this._panel.addEventListener("dragend", this); 4886 // Wait until the next tick to resolve so all popupshown 4887 // handlers have a chance to run before our promise resolution 4888 // handlers do. 4889 Services.tm.dispatchToMainThread(resolve); 4890 }, 4891 { once: true } 4892 ); 4893 4894 let openPanel = () => { 4895 // Ensure we update the gEditUIVisible flag when opening the popup, in 4896 // case the edit controls are in it. 4897 this._panel.addEventListener( 4898 "popupshowing", 4899 () => { 4900 doc.defaultView.updateEditUIVisibility(); 4901 }, 4902 { once: true } 4903 ); 4904 4905 this._panel.addEventListener( 4906 "popuphidden", 4907 () => { 4908 if (!popupshown) { 4909 // The panel was hidden again before it was shown. This can break 4910 // consumers waiting for the panel to show. So we try again. 4911 openPanel(); 4912 } 4913 }, 4914 { once: true } 4915 ); 4916 4917 PanelMultiView.openPopup(this._panel, anchor || this._chevron, { 4918 triggerEvent: aEvent, 4919 }); 4920 this._chevron.open = true; 4921 }; 4922 4923 openPanel(); 4924 }); 4925 }, 4926 4927 /** 4928 * Exposes whether _onOverflow is currently running. 4929 */ 4930 isHandlingOverflow() { 4931 return !!this._onOverflowHandle; 4932 }, 4933 4934 _onClickChevron(aEvent) { 4935 if (this._chevron.open) { 4936 this._chevron.open = false; 4937 PanelMultiView.hidePopup(this._panel); 4938 } else if (this._panel.state != "hiding" && !this._chevron.disabled) { 4939 this.show(aEvent); 4940 } 4941 }, 4942 4943 _onPanelHiding(aEvent) { 4944 if (aEvent.target != this._panel) { 4945 // Ignore context menus, <select> popups, etc. 4946 return; 4947 } 4948 this._chevron.open = false; 4949 this._panel.removeEventListener("dragover", this); 4950 this._panel.removeEventListener("dragend", this); 4951 let doc = aEvent.target.ownerDocument; 4952 doc.defaultView.updateEditUIVisibility(); 4953 let contextMenuId = this._panel.getAttribute("context"); 4954 if (contextMenuId) { 4955 let contextMenu = doc.getElementById(contextMenuId); 4956 gELS.removeSystemEventListener(contextMenu, "command", this, true); 4957 } 4958 }, 4959 4960 /** 4961 * Returns an array with two elements, the first one a boolean representing 4962 * whether we're overflowing, the second one a number representing the 4963 * maximum width items may occupy so we don't overflow. 4964 */ 4965 async _getOverflowInfo() { 4966 let win = this._target.ownerGlobal; 4967 let totalAvailWidth; 4968 let targetWidth; 4969 await win.promiseDocumentFlushed(() => { 4970 let style = win.getComputedStyle(this._toolbar); 4971 totalAvailWidth = 4972 this._toolbar.clientWidth - 4973 parseFloat(style.paddingLeft) - 4974 parseFloat(style.paddingRight); 4975 for (let child of this._toolbar.children) { 4976 if (child.nodeName == "panel") { 4977 // Ugh. PanelUI.showSubView puts customizationui-widget-panel 4978 // directly into the toolbar. (bug 1158583) 4979 continue; 4980 } 4981 style = win.getComputedStyle(child); 4982 if ( 4983 style.display == "none" || 4984 (style.position != "static" && style.position != "relative") 4985 ) { 4986 continue; 4987 } 4988 totalAvailWidth -= 4989 parseFloat(style.marginLeft) + parseFloat(style.marginRight); 4990 if (child != this._target) { 4991 totalAvailWidth -= child.clientWidth; 4992 } 4993 } 4994 targetWidth = this._target.clientWidth; 4995 }); 4996 log.debug( 4997 `Getting overflow info: target width: ${targetWidth}; available width: ${totalAvailWidth}` 4998 ); 4999 return [targetWidth > totalAvailWidth, totalAvailWidth]; 5000 }, 5001 5002 /** 5003 * Handle overflow in the toolbar by moving items to the overflow menu. 5004 */ 5005 async _onOverflow() { 5006 if (!this._enabled) { 5007 return; 5008 } 5009 5010 let win = this._target.ownerGlobal; 5011 let onOverflowHandle = {}; 5012 this._onOverflowHandle = onOverflowHandle; 5013 5014 let [isOverflowing] = await this._getOverflowInfo(); 5015 5016 // Stop if the window has closed or if we re-enter while waiting for 5017 // layout. 5018 if (win.closed || this._onOverflowHandle != onOverflowHandle) { 5019 log.debug("Window closed or another overflow handler started."); 5020 return; 5021 } 5022 5023 let child = this._target.lastElementChild; 5024 while (child && isOverflowing) { 5025 let prevChild = child.previousElementSibling; 5026 5027 if (child.getAttribute("overflows") != "false") { 5028 this._collapsed.set(child.id, this._target.clientWidth); 5029 child.setAttribute("overflowedItem", true); 5030 child.setAttribute("cui-anchorid", this._chevron.id); 5031 CustomizableUIInternal.ensureButtonContextMenu( 5032 child, 5033 this._toolbar, 5034 true 5035 ); 5036 CustomizableUIInternal.notifyListeners( 5037 "onWidgetOverflow", 5038 child, 5039 this._target 5040 ); 5041 5042 this._list.insertBefore(child, this._list.firstElementChild); 5043 if (!CustomizableUI.isSpecialWidget(child.id)) { 5044 this._toolbar.setAttribute("overflowing", "true"); 5045 } 5046 } 5047 child = prevChild; 5048 [isOverflowing] = await this._getOverflowInfo(); 5049 // Stop if the window has closed or if we re-enter while waiting for 5050 // layout. 5051 if (win.closed || this._onOverflowHandle != onOverflowHandle) { 5052 log.debug("Window closed or another overflow handler started."); 5053 return; 5054 } 5055 } 5056 5057 win.UpdateUrlbarSearchSplitterState(); 5058 5059 this._onOverflowHandle = null; 5060 }, 5061 5062 _onResize(aEvent) { 5063 // Ignore bubbled-up resize events. 5064 if (aEvent.target != aEvent.currentTarget) { 5065 return; 5066 } 5067 this._checkOverflow(); 5068 }, 5069 5070 /** 5071 * Try to move toolbar items back to the toolbar from the overflow menu. 5072 * @param {boolean} shouldMoveAllItems 5073 * Whether we should move everything (e.g. because we're being disabled) 5074 * @param {number} totalAvailWidth 5075 * Optional; the width of the area in which we can put things. 5076 * Some consumers pass this to avoid reflows. 5077 * While there are items in the list, this width won't change, and so 5078 * we can avoid flushing layout by providing it and/or caching it. 5079 * Note that if `shouldMoveAllItems` is true, we never need the width 5080 * anyway. 5081 */ 5082 async _moveItemsBackToTheirOrigin(shouldMoveAllItems, totalAvailWidth) { 5083 log.debug( 5084 `Attempting to move ${shouldMoveAllItems ? "all" : "some"} items back` 5085 ); 5086 let placements = gPlacements.get(this._toolbar.id); 5087 let win = this._target.ownerGlobal; 5088 let moveItemsBackToTheirOriginHandle = {}; 5089 this._moveItemsBackToTheirOriginHandle = moveItemsBackToTheirOriginHandle; 5090 5091 while (this._list.firstElementChild) { 5092 let child = this._list.firstElementChild; 5093 let minSize = this._collapsed.get(child.id); 5094 log.debug(`Considering moving ${child.id} back, minSize: ${minSize}`); 5095 5096 if (!shouldMoveAllItems && minSize) { 5097 if (!totalAvailWidth) { 5098 [, totalAvailWidth] = await this._getOverflowInfo(); 5099 5100 // If the window has closed or if we re-enter because we were waiting 5101 // for layout, stop. 5102 if ( 5103 win.closed || 5104 this._moveItemsBackToTheirOriginHandle != 5105 moveItemsBackToTheirOriginHandle 5106 ) { 5107 log.debug( 5108 "Window closed or _moveItemsBackToTheirOrigin called again." 5109 ); 5110 return; 5111 } 5112 } 5113 if (totalAvailWidth <= minSize) { 5114 log.debug( 5115 `Need ${minSize} but width is ${totalAvailWidth} so bailing` 5116 ); 5117 break; 5118 } 5119 } 5120 5121 log.debug(`Moving ${child.id} back`); 5122 this._collapsed.delete(child.id); 5123 let beforeNodeIndex = placements.indexOf(child.id) + 1; 5124 // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list, 5125 // we're inserting it at the end. This will mean first-in, first-out (more or less) 5126 // leading to as little change in order as possible. 5127 if (beforeNodeIndex == 0) { 5128 beforeNodeIndex = placements.length; 5129 } 5130 let inserted = false; 5131 for (; beforeNodeIndex < placements.length; beforeNodeIndex++) { 5132 let beforeNode = this._target.getElementsByAttribute( 5133 "id", 5134 placements[beforeNodeIndex] 5135 )[0]; 5136 // Unfortunately, XUL add-ons can mess with nodes after they are inserted, 5137 // and this breaks the following code if the button isn't where we expect 5138 // it to be (ie not a child of the target). In this case, ignore the node. 5139 if (beforeNode && this._target == beforeNode.parentElement) { 5140 this._target.insertBefore(child, beforeNode); 5141 inserted = true; 5142 break; 5143 } 5144 } 5145 if (!inserted) { 5146 this._target.appendChild(child); 5147 } 5148 child.removeAttribute("cui-anchorid"); 5149 child.removeAttribute("overflowedItem"); 5150 CustomizableUIInternal.ensureButtonContextMenu(child, this._target); 5151 CustomizableUIInternal.notifyListeners( 5152 "onWidgetUnderflow", 5153 child, 5154 this._target 5155 ); 5156 } 5157 5158 win.UpdateUrlbarSearchSplitterState(); 5159 5160 let collapsedWidgetIds = Array.from(this._collapsed.keys()); 5161 if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) { 5162 this._toolbar.removeAttribute("overflowing"); 5163 } 5164 5165 this._moveItemsBackToTheirOriginHandle = null; 5166 }, 5167 5168 async _checkOverflow() { 5169 if (!this._enabled) { 5170 return; 5171 } 5172 5173 let win = this._target.ownerGlobal; 5174 if (win.document.documentElement.hasAttribute("inDOMFullscreen")) { 5175 // Toolbars are hidden and cannot be made visible in DOM fullscreen mode 5176 // so there's nothing to do here. 5177 return; 5178 } 5179 5180 log.debug("Checking overflow"); 5181 let [isOverflowing, totalAvailWidth] = await this._getOverflowInfo(); 5182 if (win.closed) { 5183 return; 5184 } 5185 5186 if (isOverflowing) { 5187 this._onOverflow(); 5188 } else { 5189 this._moveItemsBackToTheirOrigin(false, totalAvailWidth); 5190 } 5191 }, 5192 5193 _disable() { 5194 this._moveItemsBackToTheirOrigin(true); 5195 this._enabled = false; 5196 }, 5197 5198 _enable() { 5199 this._enabled = true; 5200 this._checkOverflow(); 5201 }, 5202 5203 onWidgetBeforeDOMChange(aNode, aNextNode, aContainer) { 5204 if ( 5205 !this._enabled || 5206 (aContainer != this._target && aContainer != this._list) 5207 ) { 5208 return; 5209 } 5210 // When we (re)move an item, update all the items that come after it in the list 5211 // with the minsize *of the item before the to-be-removed node*. This way, we 5212 // ensure that we try to move items back as soon as that's possible. 5213 if (aNode.parentNode == this._list) { 5214 let updatedMinSize; 5215 if (aNode.previousElementSibling) { 5216 updatedMinSize = this._collapsed.get(aNode.previousElementSibling.id); 5217 } else { 5218 // Force (these) items to try to flow back into the bar: 5219 updatedMinSize = 1; 5220 } 5221 let nextItem = aNode.nextElementSibling; 5222 while (nextItem) { 5223 this._collapsed.set(nextItem.id, updatedMinSize); 5224 nextItem = nextItem.nextElementSibling; 5225 } 5226 } 5227 }, 5228 5229 onWidgetAfterDOMChange(aNode, aNextNode, aContainer) { 5230 if ( 5231 !this._enabled || 5232 (aContainer != this._target && aContainer != this._list) 5233 ) { 5234 return; 5235 } 5236 5237 let nowOverflowed = aNode.parentNode == this._list; 5238 let wasOverflowed = this._collapsed.has(aNode.id); 5239 5240 // If this wasn't overflowed before... 5241 if (!wasOverflowed) { 5242 // ... but it is now, then we added to the overflow panel. 5243 if (nowOverflowed) { 5244 // We could be the first item in the overflow panel if we're being inserted 5245 // before the previous first item in it. We can't assume the minimum 5246 // size is the same (because the other item might be much wider), so if 5247 // there is no previous item, just allow this item to be put back in the 5248 // toolbar immediately by specifying a very low minimum size. 5249 let sourceOfMinSize = aNode.previousElementSibling; 5250 let minSize = sourceOfMinSize 5251 ? this._collapsed.get(sourceOfMinSize.id) 5252 : 1; 5253 this._collapsed.set(aNode.id, minSize); 5254 aNode.setAttribute("cui-anchorid", this._chevron.id); 5255 aNode.setAttribute("overflowedItem", true); 5256 CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer, true); 5257 CustomizableUIInternal.notifyListeners( 5258 "onWidgetOverflow", 5259 aNode, 5260 this._target 5261 ); 5262 } 5263 } else if (!nowOverflowed) { 5264 // If it used to be overflowed... 5265 // ... and isn't anymore, let's remove our bookkeeping: 5266 this._collapsed.delete(aNode.id); 5267 aNode.removeAttribute("cui-anchorid"); 5268 aNode.removeAttribute("overflowedItem"); 5269 CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer); 5270 CustomizableUIInternal.notifyListeners( 5271 "onWidgetUnderflow", 5272 aNode, 5273 this._target 5274 ); 5275 5276 let collapsedWidgetIds = Array.from(this._collapsed.keys()); 5277 if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) { 5278 this._toolbar.removeAttribute("overflowing"); 5279 } 5280 } else if (aNode.previousElementSibling) { 5281 // but if it still is, it must have changed places. Bookkeep: 5282 let prevId = aNode.previousElementSibling.id; 5283 let minSize = this._collapsed.get(prevId); 5284 this._collapsed.set(aNode.id, minSize); 5285 } 5286 5287 // We might overflow now if an item was added, or we may be able to move 5288 // stuff back into the toolbar if an item was removed. 5289 this._checkOverflow(); 5290 }, 5291 5292 findOverflowedInsertionPoints(aNode) { 5293 let newNodeCanOverflow = aNode.getAttribute("overflows") != "false"; 5294 let areaId = this._toolbar.id; 5295 let placements = gPlacements.get(areaId); 5296 let nodeIndex = placements.indexOf(aNode.id); 5297 let nodeBeforeNewNodeIsOverflown = false; 5298 5299 let loopIndex = -1; 5300 // Loop through placements to find where to insert this item. 5301 // As soon as we find an overflown widget, we will only 5302 // insert in the overflow panel (this is why we check placements 5303 // before the desired location for the new node). Once we pass 5304 // the desired location of the widget, we look for placement ids 5305 // that actually have DOM equivalents to insert before. If all 5306 // else fails, we insert at the end of either the overflow list 5307 // or the toolbar target. 5308 while (++loopIndex < placements.length) { 5309 let nextNodeId = placements[loopIndex]; 5310 if (loopIndex > nodeIndex) { 5311 // Note that if aNode is in a template, its `ownerDocument` is *not* 5312 // going to be the browser.xhtml document, so we cannot rely on it. 5313 let nextNode = this._toolbar.ownerDocument.getElementById(nextNodeId); 5314 // If the node we're inserting can overflow, and the next node 5315 // in the toolbar is overflown, we should insert this node 5316 // in the overflow panel before it. 5317 if ( 5318 newNodeCanOverflow && 5319 this._collapsed.has(nextNodeId) && 5320 nextNode && 5321 nextNode.parentNode == this._list 5322 ) { 5323 return [this._list, nextNode]; 5324 } 5325 // Otherwise (if either we can't overflow, or the previous node 5326 // wasn't overflown), and the next node is in the toolbar itself, 5327 // insert the node in the toolbar. 5328 if ( 5329 (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) && 5330 nextNode && 5331 (nextNode.parentNode == this._target || 5332 // Also check if the next node is in a customization wrapper 5333 // (toolbarpaletteitem). We don't need to do this for the 5334 // overflow case because overflow is disabled in customize mode. 5335 (nextNode.parentNode.localName == "toolbarpaletteitem" && 5336 nextNode.parentNode.parentNode == this._target)) 5337 ) { 5338 return [this._target, nextNode]; 5339 } 5340 } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) { 5341 nodeBeforeNewNodeIsOverflown = true; 5342 } 5343 } 5344 5345 let containerForAppending = 5346 this._collapsed.size && newNodeCanOverflow ? this._list : this._target; 5347 return [containerForAppending, null]; 5348 }, 5349 5350 getContainerFor(aNode) { 5351 if (aNode.getAttribute("overflowedItem") == "true") { 5352 return this._list; 5353 } 5354 return this._target; 5355 }, 5356 5357 _hideTimeoutId: null, 5358 _showWithTimeout() { 5359 this.show().then(() => { 5360 let window = this._toolbar.ownerGlobal; 5361 if (this._hideTimeoutId) { 5362 window.clearTimeout(this._hideTimeoutId); 5363 } 5364 this._hideTimeoutId = window.setTimeout(() => { 5365 if (!this._panel.firstElementChild.matches(":hover")) { 5366 PanelMultiView.hidePopup(this._panel); 5367 } 5368 }, OVERFLOW_PANEL_HIDE_DELAY_MS); 5369 }); 5370 }, 5371}; 5372 5373CustomizableUIInternal.initialize(); 5374