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