CustomizableUI.sys.mjs (295143B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 6 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 7 import { SearchWidgetTracker } from "moz-src:///browser/components/customizableui/SearchWidgetTracker.sys.mjs"; 8 9 const lazy = {}; 10 11 ChromeUtils.defineESModuleGetters(lazy, { 12 AddonManager: "resource://gre/modules/AddonManager.sys.mjs", 13 AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs", 14 BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", 15 CustomizableWidgets: 16 "moz-src:///browser/components/customizableui/CustomizableWidgets.sys.mjs", 17 HomePage: "resource:///modules/HomePage.sys.mjs", 18 PanelMultiView: 19 "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs", 20 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 21 ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs", 22 }); 23 24 ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () { 25 const kUrl = 26 "chrome://browser/locale/customizableui/customizableWidgets.properties"; 27 return Services.strings.createBundle(kUrl); 28 }); 29 30 const kDefaultThemeID = "default-theme@mozilla.org"; 31 32 const kSpecialWidgetPfx = "customizableui-special-"; 33 34 const kPrefCustomizationState = "browser.uiCustomization.state"; 35 const kPrefCustomizationHorizontalTabstrip = 36 "browser.uiCustomization.horizontalTabstrip"; 37 const kPrefCustomizationHorizontalTabsBackup = 38 "browser.uiCustomization.horizontalTabsBackup"; 39 const kPrefCustomizationNavBarWhenVerticalTabs = 40 "browser.uiCustomization.navBarWhenVerticalTabs"; 41 const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; 42 const kPrefCustomizationDebug = "browser.uiCustomization.debug"; 43 const kPrefDrawInTitlebar = "browser.tabs.inTitlebar"; 44 const kPrefUIDensity = "browser.uidensity"; 45 const kPrefAutoTouchMode = "browser.touchmode.auto"; 46 const kPrefAutoHideDownloadsButton = "browser.download.autohideButton"; 47 const kPrefProtonToolbarVersion = "browser.proton.toolbar.version"; 48 const kPrefHomeButtonUsed = "browser.engagement.home-button.has-used"; 49 const kPrefLibraryButtonUsed = "browser.engagement.library-button.has-used"; 50 const kPrefSidebarButtonUsed = "browser.engagement.sidebar-button.has-used"; 51 const kPrefSidebarRevampEnabled = "sidebar.revamp"; 52 const kPrefSidebarVerticalTabsEnabled = "sidebar.verticalTabs"; 53 const kPrefSidebarPositionStartEnabled = "sidebar.position_start"; 54 55 const kExpectedWindowURL = AppConstants.BROWSER_CHROME_URL; 56 57 var gDefaultTheme; 58 var gSelectedTheme; 59 60 /** 61 * The keys are the handlers that are fired when the event type (the value) 62 * is fired on the subview. A widget that provides a subview has the option 63 * of providing onViewShowing and onViewHiding event handlers. 64 */ 65 const kSubviewEvents = ["ViewShowing", "ViewHiding"]; 66 67 /** 68 * The current version. We can use this to auto-add new default widgets as necessary. 69 * (would be const but isn't because of testing purposes) 70 */ 71 var kVersion = 23; 72 73 /** 74 * The current version for base browser. 75 */ 76 var kVersionBaseBrowser = 2; 77 const NoScriptId = "_73a6fe31-595d-460b-a920-fcc0f8843232_-browser-action"; 78 79 /** 80 * The current version for tor browser. 81 */ 82 var kVersionTorBrowser = 1; 83 84 /** 85 * Buttons removed from built-ins by version they were removed. kVersion must be 86 * bumped any time a new id is added to this. Use the button id as key, and 87 * version the button is removed in as the value. e.g. "pocket-button": 5 88 */ 89 var ObsoleteBuiltinButtons = { 90 "feed-button": 15, 91 }; 92 93 /** 94 * gPalette is a map of every widget that CustomizableUI.sys.mjs knows about, keyed 95 * on their IDs. 96 */ 97 var gPalette = new Map(); 98 99 /** 100 * gAreas maps area IDs to Sets of properties about those areas. An area is a 101 * place where a widget can be put. 102 */ 103 var gAreas = new Map(); 104 105 /** 106 * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets 107 * are placed within that area (either directly in the area node, or in the 108 * customizationTarget of the node). 109 */ 110 var gPlacements = new Map(); 111 112 /** 113 * gFuturePlacements represent placements that will happen for areas that have 114 * not yet loaded (due to lazy-loading). This can occur when add-ons register 115 * widgets. 116 */ 117 var gFuturePlacements = new Map(); 118 119 var gSupportedWidgetTypes = new Set([ 120 // A button that does a command. 121 "button", 122 123 // A button that opens a view in a panel (or in a subview of the panel). 124 "view", 125 126 // A combination of the above, which looks different depending on whether it's 127 // located in the toolbar or in the panel: When located in the toolbar, shown 128 // as a combined item of a button and a dropmarker button. The button triggers 129 // the command and the dropmarker button opens the view. When located in the 130 // panel, shown as one item which opens the view, and the button command 131 // cannot be triggered separately. 132 "button-and-view", 133 134 // A custom widget that defines its own markup. 135 "custom", 136 ]); 137 138 /** 139 * gPanelsForWindow is a list of known panels in a window which we may need to close 140 * should command events fire which target them. 141 * 142 * @type {WeakMap<Element, Set<Element>>} 143 */ 144 var gPanelsForWindow = new WeakMap(); 145 146 /** 147 * gSeenWidgets remembers which widgets the user has seen for the first time 148 * before. This way, if a new widget is created, and the user has not seen it 149 * before, it can be put in its default location. Otherwise, it remains in the 150 * palette. 151 */ 152 var gSeenWidgets = new Set(); 153 154 /** 155 * gDirtyAreaCache is a set of area IDs for areas where items have been added, 156 * moved or removed at least once. This set is persisted, and is used to 157 * optimize building of toolbars in the default case where no toolbars should 158 * be "dirty". 159 */ 160 var gDirtyAreaCache = new Set(); 161 162 /** 163 * gPendingBuildAreas is a map from area IDs to map from build nodes to their 164 * existing children at the time of node registration, that are waiting 165 * for the area to be registered 166 */ 167 var gPendingBuildAreas = new Map(); 168 169 var gSavedState = null; 170 var gRestoring = false; 171 var gDirty = false; 172 var gInBatchStack = 0; 173 var gResetting = false; 174 var gUndoResetting = false; 175 176 /** 177 * gBuildAreas maps area IDs to actual area nodes within browser windows. 178 */ 179 var gBuildAreas = new Map(); 180 181 /** 182 * gBuildWindows is a map of windows that have registered build areas, mapped 183 * to a Set of known toolboxes in that window. 184 */ 185 var gBuildWindows = new Map(); 186 187 var gNewElementCount = 0; 188 var gGroupWrapperCache = new Map(); 189 var gSingleWrapperCache = new WeakMap(); 190 var gListeners = new Set(); 191 192 var gUIStateBeforeReset = { 193 uiCustomizationState: null, 194 drawInTitlebar: null, 195 currentTheme: null, 196 uiDensity: null, 197 autoTouchMode: null, 198 sidebarPositionStart: null, 199 }; 200 201 /* 202 * The current tab orientation: initially null until initialization, 203 * true for vertical, false for horizontal 204 */ 205 var gCurrentVerticalTabs = null; 206 207 XPCOMUtils.defineLazyPreferenceGetter( 208 lazy, 209 "gDebuggingEnabled", 210 kPrefCustomizationDebug, 211 false, 212 (pref, oldVal, newVal) => { 213 if (typeof lazy.log != "undefined") { 214 lazy.log.maxLogLevel = newVal ? "all" : "log"; 215 } 216 } 217 ); 218 219 XPCOMUtils.defineLazyPreferenceGetter( 220 lazy, 221 "resetPBMToolbarButtonEnabled", 222 "browser.privatebrowsing.resetPBM.enabled", 223 false 224 ); 225 226 XPCOMUtils.defineLazyPreferenceGetter( 227 lazy, 228 "sidebarRevampEnabled", 229 "sidebar.revamp", 230 false, 231 (pref, oldVal, newVal) => { 232 if (!newVal) { 233 return; 234 } 235 let navbarPlacements = CustomizableUI.getWidgetIdsInArea( 236 CustomizableUI.AREA_NAVBAR 237 ); 238 if (!navbarPlacements.includes("sidebar-button")) { 239 CustomizableUI.addWidgetToArea( 240 "sidebar-button", 241 CustomizableUI.AREA_NAVBAR, 242 Services.prefs.getBoolPref(kPrefSidebarPositionStartEnabled, true) 243 ? 0 244 : undefined // Adds to the end of navbar if position_start is false. 245 ); 246 } 247 // Ensure CUI knows to not restore this button if the user later removes it 248 let prefId = "browser.toolbarbuttons.introduced.sidebar-button"; 249 Services.prefs.setBoolPref(prefId, true); 250 } 251 ); 252 253 XPCOMUtils.defineLazyPreferenceGetter( 254 lazy, 255 "verticalTabsPref", 256 "sidebar.verticalTabs", 257 false, 258 (pref, oldVal, newVal) => { 259 lazy.log.debug( 260 `sidebar.verticalTabs change handler, calling updateTabStripOrientation with value: ${newVal}, gCurrentVerticalTabs: ${gCurrentVerticalTabs}` 261 ); 262 CustomizableUIInternal.updateTabStripOrientation(); 263 } 264 ); 265 266 XPCOMUtils.defineLazyPreferenceGetter( 267 lazy, 268 "horizontalPlacementsPref", 269 kPrefCustomizationHorizontalTabstrip, 270 "" 271 ); 272 273 XPCOMUtils.defineLazyPreferenceGetter( 274 lazy, 275 "verticalPlacementsPref", 276 kPrefCustomizationNavBarWhenVerticalTabs, 277 "" 278 ); 279 280 ChromeUtils.defineLazyGetter(lazy, "log", () => { 281 let { ConsoleAPI } = ChromeUtils.importESModule( 282 "resource://gre/modules/Console.sys.mjs" 283 ); 284 let consoleOptions = { 285 maxLogLevel: lazy.gDebuggingEnabled ? "all" : "log", 286 prefix: "CustomizableUI", 287 }; 288 return new ConsoleAPI(consoleOptions); 289 }); 290 291 /** 292 * This is the internal, private implementation of most of the CustomizableUI 293 * API. This is intentionally not exported, but is instead called directly 294 * from within this module via the exported CustomizableUI object, which 295 * allows us to get a type of encapsulation. 296 */ 297 var CustomizableUIInternal = { 298 /** 299 * Main entrypoint to initializing the CustomizableUI singleton. This is 300 * called once the very first time this module is evaluated anywhere, via a 301 * a call at the very end of this module file. 302 * 303 * This sets up observers, registers built-in widgets, and loads the saved 304 * customization state from preferences, and performs any migration on that 305 * loaded state. 306 */ 307 initialize() { 308 lazy.log.debug("Initializing"); 309 310 lazy.AddonManagerPrivate.databaseReady.then(async () => { 311 lazy.AddonManager.addAddonListener(this); 312 313 let addons = await lazy.AddonManager.getAddonsByTypes(["theme"]); 314 gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID); 315 gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme; 316 }); 317 318 this.addListener(this); 319 this.defineBuiltInWidgets(); 320 this.loadSavedState(); 321 this.updateForNewVersion(); 322 this.updateForNewProtonVersion(); 323 this.markObsoleteBuiltinButtonsSeen(); 324 this.updateForBaseBrowser(); 325 this.updateForTorBrowser(); 326 327 this.registerArea( 328 CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, 329 { 330 type: CustomizableUI.TYPE_PANEL, 331 defaultPlacements: [], 332 anchor: "nav-bar-overflow-button", 333 }, 334 true 335 ); 336 337 this.registerArea( 338 CustomizableUI.AREA_ADDONS, 339 { 340 type: CustomizableUI.TYPE_PANEL, 341 defaultPlacements: [], 342 anchor: "unified-extensions-button", 343 }, 344 false 345 ); 346 347 let navbarPlacements = [ 348 lazy.sidebarRevampEnabled ? "sidebar-button" : null, 349 "back-button", 350 "forward-button", 351 "stop-reload-button", 352 Services.policies.isAllowed("removeHomeButtonByDefault") 353 ? null 354 : "home-button", 355 "vertical-spacer", 356 "urlbar-container", 357 // Don't want springs either side of the urlbar. tor-browser#41736 358 // Base-browser additions tor-browser#41736. If you want to add to, remove 359 // from, or rearrange this list, then bump the kVersionBaseBrowser and 360 // update existing saved states in _updateForBaseBrowser. 361 // Or if the change is only meant for tor-browser, bump kVersionTorBrowser 362 // instead and update the existing saved states in _updateForTorBrowser. 363 "security-level-button", 364 "new-identity-button", 365 "downloads-button", 366 AppConstants.MOZ_DEV_EDITION ? "developer-button" : null, 367 "fxa-toolbar-menu-button", 368 lazy.resetPBMToolbarButtonEnabled ? "reset-pbm-toolbar-button" : null, 369 ].filter(name => name); 370 371 this.registerArea( 372 CustomizableUI.AREA_NAVBAR, 373 { 374 type: CustomizableUI.TYPE_TOOLBAR, 375 overflowable: true, 376 defaultPlacements: navbarPlacements, 377 verticalTabsDefaultPlacements: [ 378 "firefox-view-button", 379 "alltabs-button", 380 ], 381 defaultCollapsed: false, 382 }, 383 true 384 ); 385 // navbarPlacements does not match the initial default XHTML layout. 386 // Therefore we always need to rebuild the navbar area when 387 // registerToolbarNode is called. tor-browser#41736 388 gDirtyAreaCache.add(CustomizableUI.AREA_NAVBAR); 389 390 if (!Services.appinfo.nativeMenubar) { 391 this.registerArea( 392 CustomizableUI.AREA_MENUBAR, 393 { 394 type: CustomizableUI.TYPE_TOOLBAR, 395 defaultPlacements: ["menubar-items"], 396 defaultCollapsed: true, 397 }, 398 true 399 ); 400 } 401 402 this.registerArea( 403 CustomizableUI.AREA_TABSTRIP, 404 { 405 type: CustomizableUI.TYPE_TOOLBAR, 406 defaultPlacements: [ 407 "firefox-view-button", 408 "tabbrowser-tabs", 409 "new-tab-button", 410 "alltabs-button", 411 ], 412 verticalTabsDefaultPlacements: [], 413 defaultCollapsed: null, 414 }, 415 true 416 ); 417 418 this.registerArea( 419 CustomizableUI.AREA_VERTICAL_TABSTRIP, 420 { 421 type: "toolbar", 422 defaultPlacements: [], 423 verticalTabsDefaultPlacements: ["tabbrowser-tabs"], 424 defaultCollapsed: null, 425 }, 426 true 427 ); 428 429 this.registerArea( 430 CustomizableUI.AREA_BOOKMARKS, 431 { 432 type: CustomizableUI.TYPE_TOOLBAR, 433 defaultPlacements: ["personal-bookmarks"], 434 defaultCollapsed: "newtab", 435 }, 436 true 437 ); 438 lazy.log.debug(`All the areas registered: ${[...gAreas.keys()]}`); 439 440 // At initialization, if we find vertical tabs enabled but not sidebar.revamp 441 // we'll enable revamp rather than disable vertical tabs. 442 this.reconcileSidebarPrefs(kPrefSidebarVerticalTabsEnabled); 443 444 this.initializeForTabsOrientation(CustomizableUI.verticalTabsEnabled); 445 446 SearchWidgetTracker.init(); 447 448 Services.obs.addObserver(this, "browser-set-toolbar-visibility"); 449 450 Services.prefs.addObserver(kPrefSidebarVerticalTabsEnabled, this); 451 Services.prefs.addObserver(kPrefSidebarRevampEnabled, this); 452 Services.prefs.addObserver(kPrefSidebarPositionStartEnabled, this); 453 }, 454 455 /** 456 * Implements the onEnabled method for the AddonListener interface. Called 457 * when an add-on is marked as enabled. 458 * 459 * @param {AddonInternal} addon 460 * The add-on that was enabled. 461 */ 462 onEnabled(addon) { 463 if (addon.type == "theme") { 464 gSelectedTheme = addon; 465 } 466 }, 467 468 /** 469 * Returns a new Set that contains the IDs of all built-in customizable areas. 470 * 471 * @type {Set<string>} 472 */ 473 get builtinAreas() { 474 return new Set([ 475 ...this.builtinToolbars, 476 CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, 477 CustomizableUI.AREA_ADDONS, 478 ]); 479 }, 480 481 /** 482 * Returns a new Set that contains the IDs of all built-in customizable 483 * toolbar areas. 484 * 485 * @type {Set<string>} 486 */ 487 get builtinToolbars() { 488 let toolbars = new Set([ 489 CustomizableUI.AREA_NAVBAR, 490 CustomizableUI.AREA_BOOKMARKS, 491 CustomizableUI.AREA_TABSTRIP, 492 ]); 493 if (AppConstants.platform != "macosx") { 494 toolbars.add(CustomizableUI.AREA_MENUBAR); 495 } 496 return toolbars; 497 }, 498 499 /** 500 * Goes through the list of widgets defined in CustomizableWidgets and 501 * registers them with CustomizableUI. 502 */ 503 defineBuiltInWidgets() { 504 for (let widgetDefinition of lazy.CustomizableWidgets) { 505 this.createBuiltinWidget(widgetDefinition); 506 } 507 }, 508 509 /** 510 * Runs any necessary migrations on the current saved customization state 511 * to get us to a kVersion compatible state. 512 */ 513 // eslint-disable-next-line complexity 514 updateForNewVersion() { 515 // We should still enter even if gSavedState.currentVersion >= kVersion 516 // because the per-widget pref facility is independent of versioning. 517 if (!gSavedState) { 518 // Flip all the prefs so we don't try to re-introduce later: 519 for (let [, widget] of gPalette) { 520 if (widget.defaultArea && widget._introducedInVersion === "pref") { 521 let prefId = "browser.toolbarbuttons.introduced." + widget.id; 522 Services.prefs.setBoolPref(prefId, true); 523 } 524 } 525 return; 526 } 527 528 let currentVersion = gSavedState.currentVersion; 529 for (let [id, widget] of gPalette) { 530 if (widget.defaultArea) { 531 let shouldAdd = false; 532 let shouldSetPref = false; 533 let prefId = "browser.toolbarbuttons.introduced." + widget.id; 534 if (widget._introducedInVersion === "pref") { 535 try { 536 shouldAdd = !Services.prefs.getBoolPref(prefId); 537 } catch (ex) { 538 // Pref doesn't exist: 539 shouldAdd = true; 540 } 541 shouldSetPref = shouldAdd; 542 } else if (widget._introducedInVersion > currentVersion) { 543 shouldAdd = true; 544 } else if ( 545 widget._introducedByPref && 546 Services.prefs.getBoolPref(widget._introducedByPref) 547 ) { 548 shouldSetPref = shouldAdd = !Services.prefs.getBoolPref( 549 prefId, 550 false 551 ); 552 } 553 554 if (shouldAdd) { 555 let futurePlacements = gFuturePlacements.get(widget.defaultArea); 556 if (futurePlacements) { 557 futurePlacements.add(id); 558 } else { 559 gFuturePlacements.set(widget.defaultArea, new Set([id])); 560 } 561 if (shouldSetPref) { 562 Services.prefs.setBoolPref(prefId, true); 563 } 564 } 565 } 566 } 567 568 // Nothing to migrate now if we don't have placements. 569 if (!gSavedState.placements) { 570 return; 571 } 572 573 if ( 574 currentVersion < 7 && 575 gSavedState.placements[CustomizableUI.AREA_NAVBAR] 576 ) { 577 let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 578 let newPlacements = [ 579 "back-button", 580 "forward-button", 581 "stop-reload-button", 582 "home-button", 583 ]; 584 for (let button of placements) { 585 if (!newPlacements.includes(button)) { 586 newPlacements.push(button); 587 } 588 } 589 590 if (!newPlacements.includes("sidebar-button")) { 591 newPlacements.unshift("sidebar-button"); 592 } 593 594 gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements; 595 } 596 597 if (currentVersion < 8 && gSavedState.placements["PanelUI-contents"]) { 598 let savedPanelPlacements = gSavedState.placements["PanelUI-contents"]; 599 delete gSavedState.placements["PanelUI-contents"]; 600 let defaultPlacements = [ 601 "edit-controls", 602 "zoom-controls", 603 "new-window-button", 604 "privatebrowsing-button", 605 "save-page-button", 606 "print-button", 607 "history-panelmenu", 608 "fullscreen-button", 609 "find-button", 610 "preferences-button", 611 // This widget no longer exists as of 2023, see Bug 1799009. 612 "add-ons-button", 613 "sync-button", 614 ]; 615 616 if (!AppConstants.MOZ_DEV_EDITION) { 617 defaultPlacements.splice(-1, 0, "developer-button"); 618 } 619 620 savedPanelPlacements = savedPanelPlacements.filter( 621 id => !defaultPlacements.includes(id) 622 ); 623 624 if (savedPanelPlacements.length) { 625 gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] = 626 savedPanelPlacements; 627 } 628 } 629 630 if (currentVersion < 9 && gSavedState.placements["nav-bar"]) { 631 let placements = gSavedState.placements["nav-bar"]; 632 if (placements.includes("urlbar-container")) { 633 let urlbarIndex = placements.indexOf("urlbar-container"); 634 let secondSpringIndex = urlbarIndex + 1; 635 // Insert if there isn't already a spring before the urlbar 636 if ( 637 urlbarIndex == 0 || 638 !placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring") 639 ) { 640 placements.splice(urlbarIndex, 0, "spring"); 641 // The url bar is now 1 index later, so increment the insertion point for 642 // the second spring. 643 secondSpringIndex++; 644 } 645 // If the search container is present, insert after the search container 646 // instead of after the url bar 647 let searchContainerIndex = placements.indexOf("search-container"); 648 if (searchContainerIndex != -1) { 649 secondSpringIndex = searchContainerIndex + 1; 650 } 651 if ( 652 secondSpringIndex == placements.length || 653 !placements[secondSpringIndex].startsWith( 654 kSpecialWidgetPfx + "spring" 655 ) 656 ) { 657 placements.splice(secondSpringIndex, 0, "spring"); 658 } 659 } 660 661 // Finally, replace the bookmarks menu button with the library one if present 662 if (placements.includes("bookmarks-menu-button")) { 663 let bmbIndex = placements.indexOf("bookmarks-menu-button"); 664 placements.splice(bmbIndex, 1); 665 let downloadButtonIndex = placements.indexOf("downloads-button"); 666 let libraryIndex = 667 downloadButtonIndex == -1 ? bmbIndex : downloadButtonIndex + 1; 668 placements.splice(libraryIndex, 0, "library-button"); 669 } 670 } 671 672 if (currentVersion < 10) { 673 for (let placements of Object.values(gSavedState.placements)) { 674 if (placements.includes("webcompat-reporter-button")) { 675 placements.splice(placements.indexOf("webcompat-reporter-button"), 1); 676 break; 677 } 678 } 679 } 680 681 // Move the downloads button to the default position in the navbar if it's 682 // not there already. 683 if (currentVersion < 11) { 684 let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 685 // First remove from wherever it currently lives, if anywhere: 686 for (let placements of Object.values(gSavedState.placements)) { 687 let existingIndex = placements.indexOf("downloads-button"); 688 if (existingIndex != -1) { 689 placements.splice(existingIndex, 1); 690 break; // It can only be in 1 place, so no point looking elsewhere. 691 } 692 } 693 694 // Now put the button in the navbar in the correct spot: 695 if (navbarPlacements) { 696 let insertionPoint = navbarPlacements.indexOf("urlbar-container"); 697 // Deliberately iterate to 1 past the end of the array to insert at the 698 // end if need be. 699 while (++insertionPoint < navbarPlacements.length) { 700 let widget = navbarPlacements[insertionPoint]; 701 // If we find a non-searchbar, non-spacer node, break out of the loop: 702 if ( 703 widget != "search-container" && 704 !this.matchingSpecials(widget, "spring") 705 ) { 706 break; 707 } 708 } 709 // We either found the right spot, or reached the end of the 710 // placements, so insert here: 711 navbarPlacements.splice(insertionPoint, 0, "downloads-button"); 712 } 713 } 714 715 if (currentVersion < 12) { 716 const removedButtons = [ 717 "loop-call-button", 718 "loop-button-throttled", 719 "pocket-button", 720 ]; 721 for (let placements of Object.values(gSavedState.placements)) { 722 for (let button of removedButtons) { 723 let buttonIndex = placements.indexOf(button); 724 if (buttonIndex != -1) { 725 placements.splice(buttonIndex, 1); 726 } 727 } 728 } 729 } 730 731 // Remove the old placements from the now-gone Nightly-only 732 // "New non-e10s window" button. 733 if (currentVersion < 13) { 734 for (let placements of Object.values(gSavedState.placements)) { 735 let buttonIndex = placements.indexOf("e10s-button"); 736 if (buttonIndex != -1) { 737 placements.splice(buttonIndex, 1); 738 } 739 } 740 } 741 742 // Remove unsupported custom toolbar saved placements 743 if (currentVersion < 14) { 744 for (let area in gSavedState.placements) { 745 if (!this.builtinAreas.has(area)) { 746 delete gSavedState.placements[area]; 747 } 748 } 749 } 750 751 // Add the FxA toolbar menu as the right most button item 752 if (currentVersion < 16) { 753 let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 754 // Place the menu item as the first item to the left of the hamburger menu 755 if (navbarPlacements) { 756 navbarPlacements.push("fxa-toolbar-menu-button"); 757 } 758 } 759 760 // Add firefox-view if not present 761 if (currentVersion < 18) { 762 let tabstripPlacements = 763 gSavedState.placements[CustomizableUI.AREA_TABSTRIP]; 764 if ( 765 tabstripPlacements && 766 !tabstripPlacements.includes("firefox-view-button") 767 ) { 768 tabstripPlacements.unshift("firefox-view-button"); 769 } 770 } 771 772 // Unified Extensions addon button migration, which puts any browser action 773 // buttons in the overflow menu into the addons panel instead. 774 if (currentVersion < 19) { 775 let overflowPlacements = 776 gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] || []; 777 // The most likely case is that there are no AREA_ADDONS placements, in which case the 778 // array won't exist. 779 let addonsPlacements = 780 gSavedState.placements[CustomizableUI.AREA_ADDONS] || []; 781 782 // Migration algorithm for transitioning to Unified Extensions: 783 // 784 // 1. Create two arrays, one for extension widgets, one for built-in widgets. 785 // 2. Iterate all items in the overflow panel, and push them into the 786 // appropriate array based on whether or not its an extension widget. 787 // 3. Overwrite the overflow panel placements with the built-in widgets array. 788 // 4. Prepend the extension widgets to the addonsPlacements array. Note that this 789 // does not overwrite this array as a precaution because it's possible 790 // (though pretty unlikely) that some widgets are already there. 791 // 792 // For extension widgets that were in the palette, they will be appended to the 793 // addons area when they're created within createWidget. 794 let extWidgets = []; 795 let builtInWidgets = []; 796 for (let widgetId of overflowPlacements) { 797 if (CustomizableUI.isWebExtensionWidget(widgetId)) { 798 extWidgets.push(widgetId); 799 } else { 800 builtInWidgets.push(widgetId); 801 } 802 } 803 gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] = 804 builtInWidgets; 805 gSavedState.placements[CustomizableUI.AREA_ADDONS] = [ 806 ...extWidgets, 807 ...addonsPlacements, 808 ]; 809 } 810 811 // Add the PBM reset button as the right most button item 812 if (currentVersion < 20) { 813 let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 814 // Place the button as the first item to the left of the hamburger menu 815 if ( 816 navbarPlacements && 817 !navbarPlacements.includes("reset-pbm-toolbar-button") 818 ) { 819 navbarPlacements.push("reset-pbm-toolbar-button"); 820 } 821 } 822 823 if (currentVersion < 21) { 824 // If the vertical-spacer has not yet been added, ensure its to the left of the urlbar initially 825 let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 826 if (!navbarPlacements.includes("vertical-spacer")) { 827 let urlbarContainerPosition = 828 navbarPlacements.indexOf("urlbar-container"); 829 gSavedState.placements[CustomizableUI.AREA_NAVBAR].splice( 830 urlbarContainerPosition - 1, 831 0, 832 "vertical-spacer" 833 ); 834 } 835 } 836 837 if (currentVersion < 22) { 838 if (!Services.prefs.getBoolPref(kPrefSidebarPositionStartEnabled, true)) { 839 // If the sidebar is on the right, the toolbar button is also on the right. 840 const navbarPlacements = 841 gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 842 if (navbarPlacements[0] === "sidebar-button") { 843 navbarPlacements.shift(); 844 navbarPlacements.push("sidebar-button"); 845 } 846 } 847 } 848 849 if (currentVersion < 23) { 850 const navbarPlacements = 851 gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 852 853 let buttonIndex = navbarPlacements.indexOf("save-to-pocket-button"); 854 if (buttonIndex != -1) { 855 navbarPlacements.splice(buttonIndex, 1); 856 } 857 } 858 }, 859 860 /** 861 * A separate state migration method for when we introduced the Proton 862 * retheme in 2021. Because the Proton retheme was toggle-able via a pref, 863 * it was important to have the migration separated out. Like most old 864 * migrations, this is probably a historical artifact at this point. 865 */ 866 updateForNewProtonVersion() { 867 const VERSION = 3; 868 let currentVersion = Services.prefs.getIntPref( 869 kPrefProtonToolbarVersion, 870 0 871 ); 872 if (currentVersion >= VERSION) { 873 return; 874 } 875 876 let placements = gSavedState?.placements?.[CustomizableUI.AREA_NAVBAR]; 877 878 if (!placements) { 879 // The profile was created with this version, so no need to migrate. 880 Services.prefs.setIntPref(kPrefProtonToolbarVersion, VERSION); 881 return; 882 } 883 884 // Remove the home button if it hasn't been used and is set to about:home 885 if (currentVersion < 1) { 886 let homePage = lazy.HomePage.get(); 887 if ( 888 placements.includes("home-button") && 889 !Services.prefs.getBoolPref(kPrefHomeButtonUsed) && 890 (homePage == "about:home" || homePage == "about:blank") && 891 Services.policies.isAllowed("removeHomeButtonByDefault") 892 ) { 893 placements.splice(placements.indexOf("home-button"), 1); 894 } 895 } 896 897 // Remove the library button if it hasn't been used 898 if (currentVersion < 2) { 899 if ( 900 placements.includes("library-button") && 901 !Services.prefs.getBoolPref(kPrefLibraryButtonUsed) 902 ) { 903 placements.splice(placements.indexOf("library-button"), 1); 904 } 905 } 906 907 // Remove the library button if it hasn't been used 908 if (currentVersion < 3) { 909 if ( 910 placements.includes("sidebar-button") && 911 !Services.prefs.getBoolPref(kPrefSidebarButtonUsed) 912 ) { 913 placements.splice(placements.indexOf("sidebar-button"), 1); 914 } 915 } 916 917 Services.prefs.setIntPref(kPrefProtonToolbarVersion, VERSION); 918 }, 919 920 /** 921 * When upgrading, checks to see if any built-in button definitions were 922 * just removed / marked obsolete (see ObsoleteBuiltinButtons) for the 923 * current kVersion. If so, marks those buttons as seen. 924 */ 925 markObsoleteBuiltinButtonsSeen() { 926 if (!gSavedState) { 927 return; 928 } 929 let currentVersion = gSavedState.currentVersion; 930 if (currentVersion >= kVersion) { 931 return; 932 } 933 // we're upgrading, update state if necessary 934 for (let id in ObsoleteBuiltinButtons) { 935 let version = ObsoleteBuiltinButtons[id]; 936 if (version == kVersion) { 937 gSeenWidgets.add(id); 938 gDirty = true; 939 } 940 } 941 }, 942 943 updateForBaseBrowser() { 944 if (!gSavedState) { 945 // Use the defaults. 946 return; 947 } 948 949 const currentVersion = gSavedState.currentVersionBaseBrowser; 950 951 if (currentVersion < 1) { 952 // NOTE: In base-browser/tor-browser version 12.5a5, and earlier, the 953 // toolbar was configured by setting the full JSON string for the default 954 // "browser.uiCustomization.state" preference value. The disadvantage is 955 // that we could not update this value in a way that existing users (who 956 // would have non-default preference values) would also get the desired 957 // change (e.g. for adding or removing a button). 958 // 959 // With tor-browser#41736 we want to switch to changing the toolbar 960 // dynamically like firefox. Therefore, this first version transfer simply 961 // gets the toolbar into the same state we wanted before, away from the 962 // default firefox state. 963 // 964 // If an existing user state aligned with the previous default 965 // "browser.uiCustomization.state" then this shouldn't visibly change 966 // anything. 967 // If a user explicitly customized the toolbar to go back to the firefox 968 // default, then this may undo those changes. 969 const navbarPlacements = 970 gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 971 if (navbarPlacements) { 972 const getBeforeAfterUrlbar = () => { 973 // NOTE: The urlbar is non-removable from the navbar, so should have 974 // an index. 975 const index = navbarPlacements.indexOf("urlbar-container"); 976 let after = index + 1; 977 if ( 978 after < navbarPlacements.length && 979 navbarPlacements[after] === "search-container" 980 ) { 981 // Skip past the search-container. 982 after++; 983 } 984 return { before: index - 1, after }; 985 }; 986 987 // Remove the urlbar springs either side of the urlbar. 988 const { before, after } = getBeforeAfterUrlbar(); 989 if ( 990 after < navbarPlacements.length && 991 this.matchingSpecials(navbarPlacements[after], "spring") 992 ) { 993 // Remove the spring after. 994 navbarPlacements.splice(after, 1); 995 // NOTE: The `before` index does not change. 996 } 997 if ( 998 before >= 0 && 999 this.matchingSpecials(navbarPlacements[before], "spring") 1000 ) { 1001 // Remove the spring before. 1002 navbarPlacements.splice(before, 1); 1003 } 1004 1005 // Make sure the security-level-button and new-identity-button appears 1006 // in the toolbar. 1007 for (const id of ["new-identity-button", "security-level-button"]) { 1008 let alreadyAdded = false; 1009 for (const placements of Object.values(gSavedState.placements)) { 1010 if (placements.includes(id)) { 1011 alreadyAdded = true; 1012 break; 1013 } 1014 } 1015 if (alreadyAdded) { 1016 continue; 1017 } 1018 1019 // Add to the nav-bar, after the urlbar-container. 1020 // NOTE: We have already removed the spring after the urlbar. 1021 navbarPlacements.splice(getBeforeAfterUrlbar().after, 0, id); 1022 } 1023 } 1024 1025 // Remove save-to-pocket-button. See tor-browser#18886 and 1026 // tor-browser#31602. 1027 for (const placements of Object.values(gSavedState.placements)) { 1028 let buttonIndex = placements.indexOf("save-to-pocket-button"); 1029 if (buttonIndex != -1) { 1030 placements.splice(buttonIndex, 1); 1031 } 1032 } 1033 1034 // Remove unused fields that used to be part of 1035 // "browser.uiCustomization.state". 1036 delete gSavedState.placements["PanelUI-contents"]; 1037 delete gSavedState.placements["addon-bar"]; 1038 } 1039 1040 if (currentVersion < 2) { 1041 // Matches against kVersion 19, i.e. when the unified-extensions-button 1042 // was introduced and extensions were moved from the palette to 1043 // AREA_ADDONS. 1044 // For base browser, we want the NoScript addon to be moved from the 1045 // default palette to AREA_NAVBAR, so that if it becomes shown through the 1046 // preference extensions.hideNoScript it will appear in the toolbar. 1047 // If the NoScript addon is already in AREA_NAVBAR, we instead flip the 1048 // extensions.hideNoScript preference so that it remains visible. 1049 // See tor-browser#41581. 1050 const navbarPlacements = 1051 gSavedState.placements[CustomizableUI.AREA_NAVBAR]; 1052 if (navbarPlacements) { 1053 let noScriptVisible = false; 1054 for (const [area, placements] of Object.entries( 1055 gSavedState.placements 1056 )) { 1057 const index = placements.indexOf(NoScriptId); 1058 if (index === -1) { 1059 continue; 1060 } 1061 if (area === CustomizableUI.AREA_ADDONS) { 1062 // Has been placed in the ADDONS area. 1063 // Most likely, this is an alpha or nightly user who received the 1064 // firefox update in a run before this one. In this case, we want to 1065 // match the same behaviour as a stable user: hide the button and 1066 // move it to the NAVBAR instead. 1067 placements.splice(index, 1); 1068 } else { 1069 // It is in an area other than the ADDON (and not in the palette). 1070 noScriptVisible = true; 1071 } 1072 } 1073 if (noScriptVisible) { 1074 // Keep the button where it is and make sure it is visible. 1075 Services.prefs.setBoolPref("extensions.hideNoScript", false); 1076 } else { 1077 // Should appear just before unified-extensions-button, which is 1078 // currently not part of the default placements. 1079 const placeIndex = navbarPlacements.indexOf( 1080 "unified-extensions-button" 1081 ); 1082 if (placeIndex === -1) { 1083 navbarPlacements.push(NoScriptId); 1084 } else { 1085 navbarPlacements.splice(placeIndex, 0, NoScriptId); 1086 } 1087 } 1088 } 1089 } 1090 }, 1091 1092 updateForTorBrowser() { 1093 if (!gSavedState) { 1094 // Use the defaults. 1095 return; 1096 } 1097 1098 const currentVersion = gSavedState.currentVersionTorBrowser; 1099 1100 if (currentVersion < 1) { 1101 // Remove torbutton-button, which no longer exists. 1102 for (const placements of Object.values(gSavedState.placements)) { 1103 let buttonIndex = placements.indexOf("torbutton-button"); 1104 if (buttonIndex != -1) { 1105 placements.splice(buttonIndex, 1); 1106 } 1107 } 1108 } 1109 }, 1110 1111 /** 1112 * If a new area was defined, or new default widgets for an area are defined, 1113 * this reconciles the placements of those new default widgets with the 1114 * existing customization and toolbar state of the browser. 1115 * 1116 * @param {string} aArea 1117 * The ID of the area to reconcile the default widget state for. 1118 */ 1119 placeNewDefaultWidgetsInArea(aArea) { 1120 let futurePlacedWidgets = gFuturePlacements.get(aArea); 1121 let savedPlacements = 1122 gSavedState && gSavedState.placements && gSavedState.placements[aArea]; 1123 let defaultPlacements; 1124 if ( 1125 CustomizableUI.verticalTabsEnabled && 1126 gAreas.get(aArea).has("verticalTabsDefaultPlacements") 1127 ) { 1128 defaultPlacements = gAreas 1129 .get(aArea) 1130 .get("verticalTabsDefaultPlacements"); 1131 } else { 1132 defaultPlacements = gAreas.get(aArea).get("defaultPlacements"); 1133 } 1134 if ( 1135 !savedPlacements || 1136 !savedPlacements.length || 1137 !futurePlacedWidgets || 1138 !defaultPlacements || 1139 !defaultPlacements.length 1140 ) { 1141 return; 1142 } 1143 let defaultWidgetIndex = -1; 1144 1145 for (let widgetId of futurePlacedWidgets) { 1146 let widget = gPalette.get(widgetId); 1147 if ( 1148 !widget || 1149 widget.source !== CustomizableUI.SOURCE_BUILTIN || 1150 !widget.defaultArea || 1151 !(widget._introducedInVersion || widget._introducedByPref) || 1152 savedPlacements.includes(widget.id) 1153 ) { 1154 continue; 1155 } 1156 defaultWidgetIndex = defaultPlacements.indexOf(widget.id); 1157 if (defaultWidgetIndex === -1) { 1158 continue; 1159 } 1160 // Now we know that this widget should be here by default, was newly introduced, 1161 // and we have a saved state to insert into, and a default state to work off of. 1162 // Try introducing after widgets that come before it in the default placements: 1163 for (let i = defaultWidgetIndex; i >= 0; i--) { 1164 // Special case: if the defaults list this widget as coming first, insert at the beginning: 1165 if (i === 0 && i === defaultWidgetIndex) { 1166 savedPlacements.splice(0, 0, widget.id); 1167 // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is 1168 // safe, and we won't skip any items. 1169 futurePlacedWidgets.delete(widget.id); 1170 gDirty = true; 1171 break; 1172 } 1173 // Otherwise, if we're somewhere other than the beginning, check if the previous 1174 // widget is in the saved placements. 1175 if (i) { 1176 let previousWidget = defaultPlacements[i - 1]; 1177 let previousWidgetIndex = savedPlacements.indexOf(previousWidget); 1178 if (previousWidgetIndex != -1) { 1179 savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id); 1180 futurePlacedWidgets.delete(widget.id); 1181 gDirty = true; 1182 break; 1183 } 1184 } 1185 } 1186 // The loop above either inserts the item or doesn't - either way, we can get away 1187 // with doing nothing else now; if the item remains in gFuturePlacements, we'll 1188 // add it at the end in restoreStateForArea. 1189 } 1190 this.saveState(); 1191 }, 1192 1193 /** 1194 * @see CustomizableUI.getCustomizationTarget 1195 * @param {Element|null} aElement 1196 * @returns {Element|null} 1197 */ 1198 getCustomizationTarget(aElement) { 1199 if (!aElement) { 1200 return null; 1201 } 1202 1203 if ( 1204 !aElement._customizationTarget && 1205 aElement.hasAttribute("customizable") 1206 ) { 1207 let id = aElement.getAttribute("customizationtarget"); 1208 if (id) { 1209 aElement._customizationTarget = 1210 aElement.ownerDocument.getElementById(id); 1211 } 1212 1213 if (!aElement._customizationTarget) { 1214 aElement._customizationTarget = aElement; 1215 } 1216 } 1217 1218 return aElement._customizationTarget; 1219 }, 1220 1221 /** 1222 * Given a customizable widget ID, creates and returns a WidgetGroupWrapper 1223 * or a XULGroupWrapper for that widget (depending on how the widget is 1224 * provided). This wrapper is then cached and returned for future calls 1225 * for that same widget ID. 1226 * 1227 * If the customizable widget ID cannot be resolved to a particular provider, 1228 * null is returned. 1229 * 1230 * @param {string} aWidgetId 1231 * The ID of the customizable widget to get the WidgetGroupWrapper / 1232 * XULGroupWrapper for. 1233 * @returns {WidgetGroupWrapper|XULGroupWrapper|null} 1234 * The appropriate GroupWrapper for the widget, or null if no such wrapper 1235 * can be found. 1236 */ 1237 wrapWidget(aWidgetId) { 1238 if (gGroupWrapperCache.has(aWidgetId)) { 1239 return gGroupWrapperCache.get(aWidgetId); 1240 } 1241 1242 let provider = this.getWidgetProvider(aWidgetId); 1243 if (!provider) { 1244 return null; 1245 } 1246 1247 if (provider == CustomizableUI.PROVIDER_API) { 1248 let widget = gPalette.get(aWidgetId); 1249 if (!widget.wrapper) { 1250 widget.wrapper = new WidgetGroupWrapper(widget); 1251 gGroupWrapperCache.set(aWidgetId, widget.wrapper); 1252 } 1253 return widget.wrapper; 1254 } 1255 1256 // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. 1257 // XXXgijs: this causes bugs in code that depends on widgetWrapper.provider 1258 // giving an accurate answer... filed as bug 1379821 1259 let wrapper = new XULWidgetGroupWrapper(aWidgetId); 1260 gGroupWrapperCache.set(aWidgetId, wrapper); 1261 return wrapper; 1262 }, 1263 1264 /** 1265 * @see CustomizableUI.registerArea 1266 * @param {string} aName 1267 * @param {object} aProperties 1268 * @param {boolean} aInternalCaller 1269 */ 1270 registerArea(aName, aProperties, aInternalCaller) { 1271 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { 1272 throw new Error("Invalid area name"); 1273 } 1274 1275 let areaIsKnown = gAreas.has(aName); 1276 let props = areaIsKnown ? gAreas.get(aName) : new Map(); 1277 const kImmutableProperties = new Set(["type", "overflowable"]); 1278 for (let key in aProperties) { 1279 if ( 1280 areaIsKnown && 1281 kImmutableProperties.has(key) && 1282 props.get(key) != aProperties[key] 1283 ) { 1284 throw new Error("An area cannot change the property for '" + key + "'"); 1285 } 1286 props.set(key, aProperties[key]); 1287 } 1288 // Default to a toolbar: 1289 if (!props.has("type")) { 1290 props.set("type", CustomizableUI.TYPE_TOOLBAR); 1291 } 1292 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 1293 // Check aProperties instead of props because this check is only interested 1294 // in the passed arguments, not the state of a potentially pre-existing area. 1295 if (!aInternalCaller && aProperties.defaultCollapsed) { 1296 throw new Error( 1297 "defaultCollapsed is only allowed for default toolbars." 1298 ); 1299 } 1300 if (!props.has("defaultCollapsed")) { 1301 props.set("defaultCollapsed", true); 1302 } 1303 } else if (props.has("defaultCollapsed")) { 1304 throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); 1305 } 1306 // Sanity check type: 1307 let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_PANEL]; 1308 if (!allTypes.includes(props.get("type"))) { 1309 throw new Error("Invalid area type " + props.get("type")); 1310 } 1311 1312 // And to no placements: 1313 if (!props.has("defaultPlacements")) { 1314 props.set("defaultPlacements", []); 1315 } 1316 // Sanity check default placements array: 1317 if (!Array.isArray(props.get("defaultPlacements"))) { 1318 throw new Error("Should provide an array of default placements"); 1319 } 1320 1321 if (!areaIsKnown) { 1322 gAreas.set(aName, props); 1323 1324 // Reconcile new default widgets. Have to do this before we start restoring things. 1325 this.placeNewDefaultWidgetsInArea(aName); 1326 1327 if ( 1328 props.get("type") == CustomizableUI.TYPE_TOOLBAR && 1329 !gPlacements.has(aName) 1330 ) { 1331 lazy.log.debug( 1332 `registerArea ${aName}, no gPlacements yet, nothing to restore` 1333 ); 1334 // Guarantee this area exists in gFuturePlacements, to avoid checking it in 1335 // various places elsewhere. 1336 if (!gFuturePlacements.has(aName)) { 1337 gFuturePlacements.set(aName, new Set()); 1338 } 1339 } else { 1340 this.restoreStateForArea(aName); 1341 } 1342 1343 // If we have pending build area nodes, register all of them 1344 if (gPendingBuildAreas.has(aName)) { 1345 let pendingNodes = gPendingBuildAreas.get(aName); 1346 for (let pendingNode of pendingNodes) { 1347 this.registerToolbarNode(pendingNode); 1348 } 1349 gPendingBuildAreas.delete(aName); 1350 } 1351 } 1352 }, 1353 1354 /** 1355 * @see CustomizableUI.unregisterArea 1356 * @param {string} aName 1357 * @param {boolean} [aDestroyPlacements] 1358 */ 1359 unregisterArea(aName, aDestroyPlacements) { 1360 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { 1361 throw new Error("Invalid area name"); 1362 } 1363 if (!gAreas.has(aName) && !gPlacements.has(aName)) { 1364 throw new Error("Area not registered"); 1365 } 1366 1367 // Move all the widgets out 1368 this.beginBatchUpdate(); 1369 try { 1370 let placements = gPlacements.get(aName); 1371 if (placements) { 1372 // Need to clone this array so removeWidgetFromArea doesn't modify it 1373 placements = [...placements]; 1374 placements.forEach(this.removeWidgetFromArea, this); 1375 } 1376 1377 // Delete all remaining traces. 1378 gAreas.delete(aName); 1379 // Only destroy placements when necessary: 1380 if (aDestroyPlacements) { 1381 gPlacements.delete(aName); 1382 } else { 1383 // Otherwise we need to re-set them, as removeFromArea will have emptied 1384 // them out: 1385 gPlacements.set(aName, placements); 1386 } 1387 gFuturePlacements.delete(aName); 1388 let existingAreaNodes = gBuildAreas.get(aName); 1389 if (existingAreaNodes) { 1390 for (let areaNode of existingAreaNodes) { 1391 this.notifyListeners( 1392 "onAreaNodeUnregistered", 1393 aName, 1394 this.getCustomizationTarget(areaNode), 1395 CustomizableUI.REASON_AREA_UNREGISTERED 1396 ); 1397 } 1398 } 1399 gBuildAreas.delete(aName); 1400 } finally { 1401 this.endBatchUpdate(true); 1402 } 1403 }, 1404 1405 /** 1406 * @see CustomizableUI.registerToolbarNode 1407 * @param {Element} aToolbar 1408 */ 1409 registerToolbarNode(aToolbar) { 1410 let area = aToolbar.id; 1411 if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { 1412 return; 1413 } 1414 let areaProperties = gAreas.get(area); 1415 1416 // If this area is not registered, try to do it automatically: 1417 if (!areaProperties) { 1418 if (!gPendingBuildAreas.has(area)) { 1419 gPendingBuildAreas.set(area, []); 1420 } 1421 gPendingBuildAreas.get(area).push(aToolbar); 1422 return; 1423 } 1424 1425 this.beginBatchUpdate(); 1426 try { 1427 let placements = gPlacements.get(area); 1428 if ( 1429 !placements && 1430 areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR 1431 ) { 1432 this.restoreStateForArea(area); 1433 placements = gPlacements.get(area); 1434 } 1435 1436 // For toolbars that need it, mark as dirty. 1437 let defaultPlacements = areaProperties.get("defaultPlacements"); 1438 if ( 1439 !this.builtinToolbars.has(area) || 1440 placements.length != defaultPlacements.length || 1441 !placements.every((id, i) => id == defaultPlacements[i]) 1442 ) { 1443 gDirtyAreaCache.add(area); 1444 } 1445 1446 if (areaProperties.get("overflowable")) { 1447 aToolbar.overflowable = new OverflowableToolbar(aToolbar); 1448 } 1449 1450 this.registerBuildArea(area, aToolbar); 1451 1452 // We only build the toolbar if it's been marked as "dirty". Dirty means 1453 // one of the following things: 1454 // 1) Items have been added, moved or removed from this toolbar before. 1455 // 2) The number of children of the toolbar does not match the length of 1456 // the placements array for that area. 1457 // 1458 // This notion of being "dirty" is stored in a cache which is persisted 1459 // in the saved state. 1460 // 1461 // Secondly, if the list of placements contains an API-provided widget, 1462 // we need to call `buildArea` or it won't be built and put in the toolbar. 1463 if ( 1464 gDirtyAreaCache.has(area) || 1465 placements.some(id => gPalette.has(id)) 1466 ) { 1467 this.buildArea(area, placements, aToolbar); 1468 } else { 1469 // We must have a builtin toolbar that's in the default state. We need 1470 // to only make sure that all the special nodes are correct. 1471 let specials = placements.filter(p => this.isSpecialWidget(p)); 1472 if (specials.length) { 1473 this.updateSpecialsForBuiltinToolbar(aToolbar, specials); 1474 } 1475 } 1476 this.notifyListeners( 1477 "onAreaNodeRegistered", 1478 area, 1479 this.getCustomizationTarget(aToolbar) 1480 ); 1481 } finally { 1482 lazy.log.debug( 1483 `registerToolbarNode for ${area}, tabstripAreasReady? ${this.tabstripAreasReady}` 1484 ); 1485 this.endBatchUpdate(); 1486 } 1487 }, 1488 1489 /** 1490 * For each "special" node in a toolbar (any spring, spacer, separator, or 1491 * element with an ID prefixed with kSpecialWidgetPfx), assigns the unique 1492 * ID from the saved state to the DOM nodes. These unique IDs are things like 1493 * "customizableui-special-spring30". 1494 * 1495 * @param {Element} aToolbar 1496 * The <xul:toolbar> node to update the special widget children IDs for. 1497 * @param {string[]} aSpecialIDs 1498 * An array of special node IDs from the saved placements state. 1499 */ 1500 updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) { 1501 // Nodes are going to be in the correct order, so we can do this straightforwardly: 1502 let { children } = this.getCustomizationTarget(aToolbar); 1503 for (let kid of children) { 1504 if ( 1505 this.matchingSpecials(aSpecialIDs[0], kid) && 1506 kid.getAttribute("skipintoolbarset") != "true" 1507 ) { 1508 kid.id = aSpecialIDs.shift(); 1509 } 1510 if (!aSpecialIDs.length) { 1511 return; 1512 } 1513 } 1514 }, 1515 1516 /** 1517 * This does the work of causing a customizable area to reflect the placements 1518 * that have been computed for that area. This means taking the initial 1519 * default DOM state of the area, and then modifying it to match the computed 1520 * state (either the state that had been saved in preferences, or the state 1521 * computed after doing runtime checks during initialization). 1522 * 1523 * @param {string} aAreaId 1524 * The ID of the customizable area to "build". 1525 * @param {string[]} aPlacements 1526 * The IDs of the customizable widgets that are expected to be in the 1527 * customizable area. 1528 * @param {Element} aAreaNode 1529 * The node associated with the area (as opposed to the customization 1530 * target within that area). 1531 */ 1532 buildArea(aAreaId, aPlacements, aAreaNode) { 1533 let document = aAreaNode.ownerDocument; 1534 let window = document.defaultView; 1535 let inPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(window); 1536 let container = this.getCustomizationTarget(aAreaNode); 1537 let areaIsPanel = 1538 gAreas.get(aAreaId).get("type") == CustomizableUI.TYPE_PANEL; 1539 1540 if (!container) { 1541 throw new Error( 1542 "Expected area " + aAreaId + " to have a customizationTarget attribute." 1543 ); 1544 } 1545 1546 // Restore nav-bar visibility since it may have been hidden 1547 // through a migration path (bug 938980) or an add-on. 1548 if (aAreaId == CustomizableUI.AREA_NAVBAR) { 1549 aAreaNode.collapsed = false; 1550 } 1551 1552 this.beginBatchUpdate(); 1553 1554 try { 1555 let currentNode = container.firstElementChild; 1556 let placementsToRemove = new Set(); 1557 for (let id of aPlacements) { 1558 while ( 1559 currentNode && 1560 currentNode.getAttribute("skipintoolbarset") == "true" 1561 ) { 1562 currentNode = currentNode.nextElementSibling; 1563 } 1564 1565 // Fix ids for specials and continue, for correctly placed specials. 1566 if ( 1567 currentNode && 1568 (!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) && 1569 this.matchingSpecials(id, currentNode) 1570 ) { 1571 currentNode.id = id; 1572 } 1573 if (currentNode && currentNode.id == id) { 1574 currentNode = currentNode.nextElementSibling; 1575 continue; 1576 } 1577 1578 if (this.isSpecialWidget(id) && areaIsPanel) { 1579 placementsToRemove.add(id); 1580 continue; 1581 } 1582 1583 let [provider, node] = this.getWidgetNode(id, window); 1584 if (!node) { 1585 lazy.log.debug("Unknown widget: " + id); 1586 continue; 1587 } 1588 1589 let widget = null; 1590 // If the placements have items in them which are (now) no longer removable, 1591 // we shouldn't be moving them: 1592 if (provider == CustomizableUI.PROVIDER_API) { 1593 widget = gPalette.get(id); 1594 if (!widget.removable && aAreaId != widget.defaultArea) { 1595 placementsToRemove.add(id); 1596 continue; 1597 } 1598 } else if ( 1599 provider == CustomizableUI.PROVIDER_XUL && 1600 node.parentNode != container && 1601 !this.isWidgetRemovable(node) 1602 ) { 1603 placementsToRemove.add(id); 1604 continue; 1605 } // Special widgets are always removable, so no need to check them 1606 1607 if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) { 1608 continue; 1609 } 1610 1611 if (!inPrivateWindow && widget?.hideInNonPrivateBrowsing) { 1612 continue; 1613 } 1614 1615 this.ensureButtonContextMenu(node, aAreaNode); 1616 1617 // This needs updating in case we're resetting / undoing a reset. 1618 if (widget) { 1619 widget.currentArea = aAreaId; 1620 } 1621 this.insertWidgetBefore(node, currentNode, container, aAreaId); 1622 if (gResetting) { 1623 this.notifyListeners("onWidgetReset", node, container); 1624 } else if (gUndoResetting) { 1625 this.notifyListeners("onWidgetUndoMove", node, container); 1626 } 1627 } 1628 1629 if (currentNode) { 1630 let palette = window.gNavToolbox ? window.gNavToolbox.palette : null; 1631 let limit = currentNode.previousElementSibling; 1632 let node = container.lastElementChild; 1633 while (node && node != limit) { 1634 let previousSibling = node.previousElementSibling; 1635 // Nodes opt-in to removability. If they're removable, and we haven't 1636 // seen them in the placements array, then we toss them into the palette 1637 // if one exists. If no palette exists, we just remove the node. If the 1638 // node is not removable, we leave it where it is. However, we can only 1639 // safely touch elements that have an ID - both because we depend on 1640 // IDs (or are specials), and because such elements are not intended to 1641 // be widgets (eg, titlebar-spacer elements). 1642 if ( 1643 (node.id || this.isSpecialWidget(node)) && 1644 node.getAttribute("skipintoolbarset") != "true" 1645 ) { 1646 if (this.isWidgetRemovable(node)) { 1647 if (node.id && (gResetting || gUndoResetting)) { 1648 let widget = gPalette.get(node.id); 1649 if (widget) { 1650 widget.currentArea = null; 1651 } 1652 } 1653 this.notifyDOMChange(node, null, container, true, () => { 1654 if (palette && !this.isSpecialWidget(node.id)) { 1655 palette.appendChild(node); 1656 this.removeLocationAttributes(node); 1657 } else { 1658 container.removeChild(node); 1659 } 1660 }); 1661 } else { 1662 node.setAttribute("removable", false); 1663 lazy.log.debug( 1664 "Adding non-removable widget to placements of " + 1665 aAreaId + 1666 ": " + 1667 node.id 1668 ); 1669 gPlacements.get(aAreaId).push(node.id); 1670 gDirty = true; 1671 } 1672 } 1673 node = previousSibling; 1674 } 1675 } 1676 1677 // If there are placements in here which aren't removable from their original area, 1678 // we remove them from this area's placement array. They will (have) be(en) added 1679 // to their original area's placements array in the block above this one. 1680 if (placementsToRemove.size) { 1681 let placementAry = gPlacements.get(aAreaId); 1682 for (let id of placementsToRemove) { 1683 let index = placementAry.indexOf(id); 1684 placementAry.splice(index, 1); 1685 } 1686 } 1687 1688 if (gResetting) { 1689 this.notifyListeners("onAreaReset", aAreaId, container); 1690 } 1691 } finally { 1692 this.endBatchUpdate(); 1693 } 1694 }, 1695 1696 /** 1697 * @see CustomizableUI.addPanelCloseListeners 1698 * @param {Element} aPanel 1699 */ 1700 addPanelCloseListeners(aPanel) { 1701 aPanel.addEventListener("click", this, { mozSystemGroup: true }); 1702 aPanel.addEventListener("keypress", this, { mozSystemGroup: true }); 1703 let win = aPanel.ownerGlobal; 1704 if (!gPanelsForWindow.has(win)) { 1705 gPanelsForWindow.set(win, new Set()); 1706 } 1707 gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); 1708 }, 1709 1710 /** 1711 * @see CustomizableUI.removePanelCloseListeners 1712 * @param {Element} aPanel 1713 */ 1714 removePanelCloseListeners(aPanel) { 1715 aPanel.removeEventListener("click", this, { mozSystemGroup: true }); 1716 aPanel.removeEventListener("keypress", this, { mozSystemGroup: true }); 1717 let win = aPanel.ownerGlobal; 1718 let panels = gPanelsForWindow.get(win); 1719 if (panels) { 1720 panels.delete(this._getPanelForNode(aPanel)); 1721 } 1722 }, 1723 1724 /** 1725 * Given a customizable widget node, attempts to assign it the correct 1726 * context / contextmenu attributes for the current placement of that 1727 * widget. 1728 * 1729 * @param {Element} aNode 1730 * The node to set the context / contextmenu attributes for. 1731 * @param {Element} aAreaNode 1732 * The node representing the area that aNode is currently placed within. 1733 * @param {boolean} forcePanel 1734 * True if we should force the panel context menu, regardless of the 1735 * current placement. This is mainly useful for the overflow panel, where 1736 * the placement may be the overflowable toolbar, but visually the widget 1737 * is within the overflowable toolbar panel. 1738 */ 1739 ensureButtonContextMenu(aNode, aAreaNode, forcePanel) { 1740 const kPanelItemContextMenu = "customizationPanelItemContextMenu"; 1741 1742 let currentContextMenu = 1743 aNode.getAttribute("context") || aNode.getAttribute("contextmenu"); 1744 let contextMenuForPlace; 1745 1746 if ( 1747 CustomizableUI.isWebExtensionWidget(aNode.id) && 1748 (aAreaNode?.id == CustomizableUI.AREA_ADDONS || 1749 aNode.getAttribute("overflowedItem") == "true") 1750 ) { 1751 contextMenuForPlace = null; 1752 } else { 1753 contextMenuForPlace = 1754 forcePanel || "panel" == CustomizableUI.getPlaceForItem(aAreaNode) 1755 ? kPanelItemContextMenu 1756 : null; 1757 } 1758 if (contextMenuForPlace && !currentContextMenu) { 1759 aNode.setAttribute("context", contextMenuForPlace); 1760 } else if ( 1761 currentContextMenu == kPanelItemContextMenu && 1762 contextMenuForPlace != kPanelItemContextMenu 1763 ) { 1764 aNode.removeAttribute("context"); 1765 aNode.removeAttribute("contextmenu"); 1766 } 1767 }, 1768 1769 /** 1770 * Returns the ID of the provider for a given widget. This is one of the 1771 * CustomizableUI.PROVIDER_* constants. 1772 * 1773 * @param {string} aWidgetId 1774 * The ID of the widget to get the provider for. 1775 * @returns {string} 1776 * One of the CustomizableUI.PROVIDER_* constants. 1777 */ 1778 getWidgetProvider(aWidgetId) { 1779 if (this.isSpecialWidget(aWidgetId)) { 1780 return CustomizableUI.PROVIDER_SPECIAL; 1781 } 1782 if (gPalette.has(aWidgetId)) { 1783 return CustomizableUI.PROVIDER_API; 1784 } 1785 // If this was an API widget that was destroyed, return null: 1786 if (gSeenWidgets.has(aWidgetId)) { 1787 return null; 1788 } 1789 1790 // We fall back to the XUL provider, but we don't know for sure (at this 1791 // point) whether it exists there either. So the API is technically lying. 1792 // Ideally, it would be able to return an error value (or throw an 1793 // exception) if it really didn't exist. Our code calling this function 1794 // handles that fine, but this is a public API. 1795 return CustomizableUI.PROVIDER_XUL; 1796 }, 1797 1798 /** 1799 * @typedef {string|null} GetWidgetNodeIndex0 1800 * The ID of the provider for the widget node. This is one of the constants 1801 * in CustomizableUI.PROVIDER_*. This is null if no node is found for the 1802 * widget ID. 1803 * @typedef {Element|null} GetWidgetNodeIndex1 1804 * The found node associated with a widget ID, or null if no such node can 1805 * be found. 1806 * @typedef {[GetWidgetNodeIndex0, GetWidgetNodeIndex1]} GetWidgetNodeResult 1807 */ 1808 1809 /** 1810 * For a given window, returns the node associated with a widget ID. 1811 * 1812 * @param {string} aWidgetId 1813 * The ID of the widget to get the associated node for in the window. 1814 * @param {DOMWindow} aWindow 1815 * The window to find the node for, associated with aWidgetId. 1816 * @returns {GetWidgetNodeResult} 1817 * The found node information. 1818 */ 1819 getWidgetNode(aWidgetId, aWindow) { 1820 let document = aWindow.document; 1821 1822 if (this.isSpecialWidget(aWidgetId)) { 1823 let widgetNode = 1824 document.getElementById(aWidgetId) || 1825 this.createSpecialWidget(aWidgetId, document); 1826 return [CustomizableUI.PROVIDER_SPECIAL, widgetNode]; 1827 } 1828 1829 let widget = gPalette.get(aWidgetId); 1830 if (widget) { 1831 // If we have an instance of this widget already, just use that. 1832 if (widget.instances.has(document)) { 1833 lazy.log.debug( 1834 "An instance of widget " + 1835 aWidgetId + 1836 " already exists in this " + 1837 "document. Reusing." 1838 ); 1839 return [CustomizableUI.PROVIDER_API, widget.instances.get(document)]; 1840 } 1841 1842 return [ 1843 CustomizableUI.PROVIDER_API, 1844 this.buildWidgetNode(document, widget), 1845 ]; 1846 } 1847 1848 lazy.log.debug("Searching for " + aWidgetId + " in toolbox."); 1849 let node = this.findXULWidgetInWindow(aWidgetId, aWindow); 1850 if (node) { 1851 return [CustomizableUI.PROVIDER_XUL, node]; 1852 } 1853 1854 lazy.log.debug("No node for " + aWidgetId + " found."); 1855 return [null, null]; 1856 }, 1857 1858 /** 1859 * @see CustomizableUI.registerPanelNode 1860 * @param {Element} aNode 1861 * @param {Element} aAreaId 1862 */ 1863 registerPanelNode(aNode, aAreaId) { 1864 if (gBuildAreas.has(aAreaId) && gBuildAreas.get(aAreaId).has(aNode)) { 1865 return; 1866 } 1867 1868 aNode._customizationTarget = aNode; 1869 this.addPanelCloseListeners(this._getPanelForNode(aNode)); 1870 1871 let placements = gPlacements.get(aAreaId); 1872 this.buildArea(aAreaId, placements, aNode); 1873 this.notifyListeners("onAreaNodeRegistered", aAreaId, aNode); 1874 1875 for (let child of aNode.children) { 1876 if (child.localName != "toolbarbutton") { 1877 if (child.localName == "toolbaritem") { 1878 this.ensureButtonContextMenu(child, aNode, true); 1879 } 1880 continue; 1881 } 1882 this.ensureButtonContextMenu(child, aNode, true); 1883 } 1884 1885 this.registerBuildArea(aAreaId, aNode); 1886 }, 1887 1888 /** 1889 * @type {CustomizableUIOnWidgetAddedCallback} 1890 */ 1891 onWidgetAdded(aWidgetId, aArea, _aPosition) { 1892 this.insertNode(aWidgetId, aArea, true); 1893 1894 if (!gResetting) { 1895 this._clearPreviousUIState(); 1896 } 1897 }, 1898 1899 /** 1900 * @type {CustomizableUIOnWidgetRemovedCallback} 1901 */ 1902 onWidgetRemoved(aWidgetId, aArea) { 1903 let areaNodes = gBuildAreas.get(aArea); 1904 if (!areaNodes) { 1905 return; 1906 } 1907 1908 let area = gAreas.get(aArea); 1909 let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; 1910 let isOverflowable = isToolbar && area.get("overflowable"); 1911 let showInPrivateBrowsing = gPalette.has(aWidgetId) 1912 ? gPalette.get(aWidgetId).showInPrivateBrowsing 1913 : true; 1914 let hideInNonPrivateBrowsing = 1915 gPalette.get(aWidgetId)?.hideInNonPrivateBrowsing ?? false; 1916 1917 for (let areaNode of areaNodes) { 1918 let window = areaNode.ownerGlobal; 1919 if ( 1920 !showInPrivateBrowsing && 1921 lazy.PrivateBrowsingUtils.isWindowPrivate(window) 1922 ) { 1923 continue; 1924 } 1925 1926 if ( 1927 hideInNonPrivateBrowsing && 1928 !lazy.PrivateBrowsingUtils.isWindowPrivate(window) 1929 ) { 1930 continue; 1931 } 1932 1933 let container = this.getCustomizationTarget(areaNode); 1934 let widgetNode = window.document.getElementById(aWidgetId); 1935 if (widgetNode && isOverflowable) { 1936 container = areaNode.overflowable.getContainerFor(widgetNode); 1937 } 1938 1939 if (!widgetNode || !container.contains(widgetNode)) { 1940 lazy.log.info( 1941 "Widget " + aWidgetId + " not found, unable to remove from " + aArea 1942 ); 1943 continue; 1944 } 1945 1946 this.notifyDOMChange(widgetNode, null, container, true, () => { 1947 // We remove location attributes here to make sure they're gone too when a 1948 // widget is removed from a toolbar to the palette. See bug 930950. 1949 this.removeLocationAttributes(widgetNode); 1950 // We also need to remove the panel context menu if it's there: 1951 this.ensureButtonContextMenu(widgetNode); 1952 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { 1953 container.removeChild(widgetNode); 1954 } else { 1955 window.gNavToolbox.palette.appendChild(widgetNode); 1956 } 1957 }); 1958 1959 let windowCache = gSingleWrapperCache.get(window); 1960 if (windowCache) { 1961 windowCache.delete(aWidgetId); 1962 } 1963 } 1964 if (!gResetting) { 1965 this._clearPreviousUIState(); 1966 } 1967 }, 1968 1969 /** 1970 * @type {CustomizableUIOnWidgetMovedCallback} 1971 */ 1972 onWidgetMoved(aWidgetId, aArea, _aOldPosition, _aNewPosition) { 1973 this.insertNode(aWidgetId, aArea); 1974 if (!gResetting) { 1975 this._clearPreviousUIState(); 1976 } 1977 }, 1978 1979 /** 1980 * @type {CustomizableUIOnCustomizeEnd} 1981 */ 1982 onCustomizeEnd() { 1983 this._clearPreviousUIState(); 1984 }, 1985 1986 /** 1987 * Registers a customizable area of a window with CustomizableUI such that it 1988 * can then be "built" to reflect the current stored state. 1989 * 1990 * @param {string} aAreaId 1991 * The ID of the area to be registered. 1992 * @param {Element} aAreaNode 1993 * The element for the area in the window being registered. 1994 */ 1995 registerBuildArea(aAreaId, aAreaNode) { 1996 // We ensure that the window is registered to have its customization data 1997 // cleaned up when unloading. 1998 let window = aAreaNode.ownerGlobal; 1999 if (window.closed) { 2000 return; 2001 } 2002 this.registerBuildWindow(window); 2003 2004 // Also register this build area's toolbox. 2005 if (window.gNavToolbox) { 2006 gBuildWindows.get(window).add(window.gNavToolbox); 2007 } 2008 2009 if (!gBuildAreas.has(aAreaId)) { 2010 gBuildAreas.set(aAreaId, new Set()); 2011 } 2012 2013 gBuildAreas.get(aAreaId).add(aAreaNode); 2014 2015 // Give a class to all customize targets to be used for styling in Customize Mode 2016 let customizableNode = this.getCustomizeTargetForArea(aAreaId, window); 2017 customizableNode.classList.add("customization-target"); 2018 }, 2019 2020 /** 2021 * Registers a browser window with customizable elements with CustomizableUI. 2022 * This is mainly used to set up event handlers to perform cleanups if and 2023 * when the window closes. If the window is already registered, this is a 2024 * no-op. 2025 * 2026 * @param {DOMWindow} aWindow 2027 * The window to register. 2028 */ 2029 registerBuildWindow(aWindow) { 2030 if (!gBuildWindows.has(aWindow)) { 2031 gBuildWindows.set(aWindow, new Set()); 2032 2033 aWindow.addEventListener("unload", this); 2034 aWindow.addEventListener("command", this, true); 2035 2036 this.notifyListeners("onWindowOpened", aWindow); 2037 } 2038 }, 2039 2040 /** 2041 * Unregisters a browser window that was registered with registerBuildWindow. 2042 * The onAreaNodeUnregistered callback will be called for each customizable 2043 * area within the window being unregistered. The onWidgetInstanceRemoved 2044 * will be called for each widget in the window being unregistered. Finally, 2045 * the onWindowClosed listener will be called with the window. 2046 * 2047 * @param {DOMWindow} aWindow 2048 * The window to unregister. 2049 */ 2050 unregisterBuildWindow(aWindow) { 2051 aWindow.removeEventListener("unload", this); 2052 aWindow.removeEventListener("command", this, true); 2053 gPanelsForWindow.delete(aWindow); 2054 gBuildWindows.delete(aWindow); 2055 gSingleWrapperCache.delete(aWindow); 2056 let document = aWindow.document; 2057 2058 for (let [areaId, areaNodes] of gBuildAreas) { 2059 let areaProperties = gAreas.get(areaId); 2060 for (let node of areaNodes) { 2061 if (node.ownerDocument == document) { 2062 this.notifyListeners( 2063 "onAreaNodeUnregistered", 2064 areaId, 2065 this.getCustomizationTarget(node), 2066 CustomizableUI.REASON_WINDOW_CLOSED 2067 ); 2068 if (areaProperties.get("overflowable")) { 2069 node.overflowable.uninit(); 2070 node.overflowable = null; 2071 } 2072 areaNodes.delete(node); 2073 } 2074 } 2075 } 2076 2077 for (let [, widget] of gPalette) { 2078 widget.instances.delete(document); 2079 this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); 2080 } 2081 2082 for (let [, pendingNodes] of gPendingBuildAreas) { 2083 for (let i = pendingNodes.length - 1; i >= 0; i--) { 2084 if (pendingNodes[i].ownerDocument == document) { 2085 pendingNodes.splice(i, 1); 2086 } 2087 } 2088 } 2089 2090 this.notifyListeners("onWindowClosed", aWindow); 2091 }, 2092 2093 /** 2094 * @see CustomizableUI.handleNewBrowserWindow 2095 * @param {DOMWindow} aWindow 2096 */ 2097 handleNewBrowserWindow(aWindow) { 2098 let { gNavToolbox, document, gBrowser } = aWindow; 2099 gNavToolbox.palette = document.getElementById( 2100 "BrowserToolbarPalette" 2101 ).content; 2102 2103 let isVerticalTabs = Services.prefs.getBoolPref( 2104 kPrefSidebarVerticalTabsEnabled, 2105 false 2106 ); 2107 let nonRemovables; 2108 2109 // We don't want these normally non-removable elements to get put back into the 2110 // tabstrip if we're initializing with vertical tabs. 2111 // We do this for all windows, including popups, as otherwise it's possible for 2112 // us to get confused about window state and save a blank state to prefs. 2113 if (isVerticalTabs) { 2114 nonRemovables = [gBrowser.tabContainer]; 2115 for (let elem of nonRemovables) { 2116 elem.setAttribute("removable", "true"); 2117 // tell CUI to ignore this element when it builds the toolbar areas 2118 elem.setAttribute("skipintoolbarset", "true"); 2119 } 2120 } 2121 2122 // Now register all the toolbars 2123 for (let area of CustomizableUI.areas) { 2124 let type = CustomizableUI.getAreaType(area); 2125 if (type == CustomizableUI.TYPE_TOOLBAR) { 2126 let node = document.getElementById(area); 2127 this.registerToolbarNode(node); 2128 } 2129 } 2130 2131 // Handle initial state of vertical tabs. 2132 if (isVerticalTabs) { 2133 // Show the vertical tabs toolbar 2134 aWindow.setToolbarVisibility( 2135 document.getElementById(CustomizableUI.AREA_VERTICAL_TABSTRIP), 2136 true, 2137 false, 2138 false 2139 ); 2140 let tabstripToolbar = document.getElementById( 2141 CustomizableUI.AREA_TABSTRIP 2142 ); 2143 let wasCollapsed = tabstripToolbar.collapsed; 2144 aWindow.TabBarVisibility.update(); 2145 if (tabstripToolbar.collapsed !== wasCollapsed) { 2146 let eventParams = { 2147 detail: { 2148 visible: !tabstripToolbar.collapsed, 2149 }, 2150 bubbles: true, 2151 }; 2152 let event = new CustomEvent("toolbarvisibilitychange", eventParams); 2153 tabstripToolbar.dispatchEvent(event); 2154 } 2155 2156 for (let elem of nonRemovables) { 2157 elem.setAttribute("removable", "false"); 2158 elem.removeAttribute("skipintoolbarset"); 2159 } 2160 } 2161 }, 2162 2163 /** 2164 * Sets some attributes on a customizable widget when it is introduced into 2165 * the DOM or moved around within it. Those attributes are "cui-anchorid" 2166 * and "cui-areatype". 2167 * 2168 * @param {Element} aNode 2169 * The customizable widget node being inserted or moved within the DOM. 2170 * @param {string} aAreaId 2171 * The area that the customizable widget is being moved into or within. 2172 */ 2173 setLocationAttributes(aNode, aAreaId) { 2174 let props = gAreas.get(aAreaId); 2175 if (!props) { 2176 throw new Error( 2177 "Expected area " + 2178 aAreaId + 2179 " to have a properties Map " + 2180 "associated with it." 2181 ); 2182 } 2183 2184 aNode.setAttribute("cui-areatype", props.get("type") || ""); 2185 let anchor = props.get("anchor"); 2186 if (anchor) { 2187 aNode.setAttribute("cui-anchorid", anchor); 2188 } else { 2189 aNode.removeAttribute("cui-anchorid"); 2190 } 2191 }, 2192 2193 /** 2194 * Removes any location attributes from a customizable widget node when the 2195 * node is removed from any of the registered customizable areas. 2196 * 2197 * @param {Element} aNode 2198 * The node being removed from the customizable area. 2199 */ 2200 removeLocationAttributes(aNode) { 2201 aNode.removeAttribute("cui-areatype"); 2202 aNode.removeAttribute("cui-anchorid"); 2203 }, 2204 2205 /** 2206 * Inserts a node associated with the customizable widget with ID aWidgetId 2207 * into all the areas with aAreaId across all windows. 2208 * 2209 * @param {string} aWidgetId 2210 * The ID of the customizable widget to insert into aAreaId across all 2211 * windows. 2212 * @param {string} aAreaId 2213 * The ID of the area to insert the widget into across all windows. 2214 * This method is a no-op if aAreaId is not associated with a registered 2215 * area. 2216 * @param {boolean} isNew 2217 * True if the widget is being newly inserted as opposed to moved. 2218 */ 2219 insertNode(aWidgetId, aAreaId, isNew) { 2220 let areaNodes = gBuildAreas.get(aAreaId); 2221 if (!areaNodes) { 2222 return; 2223 } 2224 2225 let placements = gPlacements.get(aAreaId); 2226 if (!placements) { 2227 lazy.log.error( 2228 "Could not find any placements for " + 2229 aAreaId + 2230 " when moving a widget." 2231 ); 2232 return; 2233 } 2234 2235 // Go through each of the nodes associated with this area and move the 2236 // widget to the requested location. 2237 for (let areaNode of areaNodes) { 2238 this.insertNodeInWindow(aWidgetId, areaNode, isNew); 2239 } 2240 }, 2241 2242 /** 2243 * Inserts a widget with ID aWidgetId into the passed area node in the 2244 * position dictated by CustomizableUI's internal positioning state for 2245 * widgets. 2246 * 2247 * @param {string} aWidgetId 2248 * The ID of the widget to insert into aAreaNode. 2249 * @param {Element} aAreaNode 2250 * The customizable area node to insert aWidgetId into. 2251 * @param {boolean} isNew 2252 * True if the widget is being inserted for the first time, instead of 2253 * moved. 2254 */ 2255 insertNodeInWindow(aWidgetId, aAreaNode, isNew) { 2256 let window = aAreaNode.ownerGlobal; 2257 let showInPrivateBrowsing = gPalette.has(aWidgetId) 2258 ? gPalette.get(aWidgetId).showInPrivateBrowsing 2259 : true; 2260 let hideInNonPrivateBrowsing = 2261 gPalette.get(aWidgetId)?.hideInNonPrivateBrowsing ?? false; 2262 2263 if ( 2264 !showInPrivateBrowsing && 2265 lazy.PrivateBrowsingUtils.isWindowPrivate(window) 2266 ) { 2267 return; 2268 } 2269 2270 if ( 2271 hideInNonPrivateBrowsing && 2272 !lazy.PrivateBrowsingUtils.isWindowPrivate(window) 2273 ) { 2274 return; 2275 } 2276 2277 let [, widgetNode] = this.getWidgetNode(aWidgetId, window); 2278 if (!widgetNode) { 2279 lazy.log.error("Widget '" + aWidgetId + "' not found, unable to move"); 2280 return; 2281 } 2282 2283 let areaId = aAreaNode.id; 2284 if (isNew) { 2285 this.ensureButtonContextMenu(widgetNode, aAreaNode); 2286 } 2287 2288 let [insertionContainer, nextNode] = this.findInsertionPoints( 2289 widgetNode, 2290 aAreaNode 2291 ); 2292 this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); 2293 }, 2294 2295 /** 2296 * @typedef {Element|null} FindInsertionPointsIndex0 2297 * The container that the node should be inserted into, or null if no such 2298 * container can be found. 2299 * @typedef {Element|null} FindInsertionPointsIndex1 2300 * The node that the insertion should occur before, or null if no such 2301 * sibling can be found. 2302 * @typedef {[FindInsertionPointsIndex0, FindInsertionPointsIndex1]} FindInsertionPointsResult 2303 */ 2304 2305 /** 2306 * Given a node for a customizable widget that may have a placement within an 2307 * area, find the location in the DOM where it makes the most sense to insert 2308 * that node. In the event of there being a placement for aNode in aAreaNode, 2309 * this insertion point will reflect the index of the node in that area's 2310 * placements array. In the event of there not being a pre-existing placement 2311 * for aNode in aAreaNode, the node will be prepended to the area. 2312 * 2313 * @param {Element} aNode 2314 * @param {Element} aAreaNode 2315 * @returns {FindInsertionPointsResult} 2316 */ 2317 findInsertionPoints(aNode, aAreaNode) { 2318 let areaId = aAreaNode.id; 2319 let props = gAreas.get(areaId); 2320 2321 // For overflowable toolbars, rely on them (because the work is more complicated): 2322 if ( 2323 props.get("type") == CustomizableUI.TYPE_TOOLBAR && 2324 props.get("overflowable") 2325 ) { 2326 return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); 2327 } 2328 2329 let container = this.getCustomizationTarget(aAreaNode); 2330 let placements = gPlacements.get(areaId); 2331 let nodeIndex = placements.indexOf(aNode.id); 2332 2333 while (++nodeIndex < placements.length) { 2334 let nextNodeId = placements[nodeIndex]; 2335 // We use aAreaNode here, because if aNode is in a template, its 2336 // `ownerDocument` is *not* going to be the browser.xhtml document, 2337 // so we cannot rely on it. 2338 let nextNode = aAreaNode.ownerDocument.getElementById(nextNodeId); 2339 // If the next placed widget exists, and is a direct child of the 2340 // container, or wrapped in a customize mode wrapper (toolbarpaletteitem) 2341 // inside the container, insert beside it. 2342 // We have to check the parent to avoid errors when the placement ids 2343 // are for nodes that are no longer customizable. 2344 if ( 2345 nextNode && 2346 (nextNode.parentNode == container || 2347 (nextNode.parentNode.localName == "toolbarpaletteitem" && 2348 nextNode.parentNode.parentNode == container)) 2349 ) { 2350 return [container, nextNode]; 2351 } 2352 } 2353 2354 return [container, null]; 2355 }, 2356 2357 /** 2358 * Inserts a node associated with a widget before some other node within a 2359 * container within a customizable area. 2360 * 2361 * @param {Element} aNode 2362 * The customizable widget node to insert. 2363 * @param {Element|null} aNextNode 2364 * The node that the inserted node should be inserted before, or null if 2365 * it should be inserted at the end of aContainer. 2366 * @param {Element} aContainer 2367 * The parent node of both aNode and aNextNode. 2368 * @param {string} aAreaId 2369 * The identifier string of the area that aNode is being inserted into. 2370 */ 2371 insertWidgetBefore(aNode, aNextNode, aContainer, aAreaId) { 2372 this.notifyDOMChange(aNode, aNextNode, aContainer, false, () => { 2373 this.setLocationAttributes(aNode, aAreaId); 2374 aContainer.insertBefore(aNode, aNextNode); 2375 }); 2376 }, 2377 2378 /** 2379 * Fires the onWidgetBeforeDOMChange event, then calls aCallback, before 2380 * firing the onWidgetAfterDOMChange event. 2381 * 2382 * @param {Element} aNode 2383 * The node that is being changed. 2384 * @param {Element|null} aNextNode 2385 * The node immediately after the one changing, or null if there is no next 2386 * node in the container. 2387 * @param {Element} aContainer 2388 * The container of the node that is being changed. 2389 * @param {boolean} aIsRemove 2390 * True iff the action about to happen is the removal of the DOM node. 2391 * @param {Function} aCallback 2392 * A synchronous function that will be called after the 2393 * onWidgetBeforeDOMChange event is fired, but before onWidgetAfterDOMChange 2394 * is fired. 2395 */ 2396 notifyDOMChange(aNode, aNextNode, aContainer, aIsRemove, aCallback) { 2397 this.notifyListeners( 2398 "onWidgetBeforeDOMChange", 2399 aNode, 2400 aNextNode, 2401 aContainer, 2402 aIsRemove 2403 ); 2404 aCallback(); 2405 this.notifyListeners( 2406 "onWidgetAfterDOMChange", 2407 aNode, 2408 aNextNode, 2409 aContainer, 2410 aIsRemove 2411 ); 2412 }, 2413 2414 /** 2415 * General event handler for CustomizableUIInternal that mostly just 2416 * dispatches to more specialized event handlers based on the event type. 2417 * 2418 * @param {CommandEvent|MouseEvent|KeyEvent|Event} aEvent 2419 */ 2420 handleEvent(aEvent) { 2421 switch (aEvent.type) { 2422 case "command": 2423 if (!this._originalEventInPanel(aEvent)) { 2424 break; 2425 } 2426 aEvent = aEvent.sourceEvent; 2427 // Fall through 2428 case "click": 2429 case "keypress": 2430 this.maybeAutoHidePanel(aEvent); 2431 break; 2432 case "unload": 2433 this.unregisterBuildWindow(aEvent.currentTarget); 2434 break; 2435 } 2436 }, 2437 2438 /** 2439 * Returns true if the CommandEvent is being fired on a target that exists 2440 * in one of the panels that CustomizableUI tracks in gPanelsForWindow. 2441 * 2442 * @param {CommandEvent} aEvent 2443 * @returns {boolean} 2444 */ 2445 _originalEventInPanel(aEvent) { 2446 let e = aEvent.sourceEvent; 2447 if (!e) { 2448 return false; 2449 } 2450 let node = this._getPanelForNode(e.target); 2451 if (!node) { 2452 return false; 2453 } 2454 let win = e.view; 2455 let panels = gPanelsForWindow.get(win); 2456 return !!panels && panels.has(node); 2457 }, 2458 2459 /** 2460 * If passed a DOM node, this will return the ID attribute for the node if it 2461 * exists. If it doesn't, it will check to see if the element localName starts 2462 * with "toolbar", and if so, return the rest of the localName after that 2463 * string. This means that for a <toolbarseparator> without an ID, this will 2464 * return "separator". If the element does not have a localName that starts 2465 * with "toolbar", this will return the empty string. 2466 * 2467 * If aNode happens to be a string instead of a DOM node, this simply returns 2468 * the string back. 2469 * 2470 * @param {Element|string} aStringOrNode 2471 * A node to try to get the special identifier for, or a string that will 2472 * be echoed back to the caller. 2473 * @returns {string} 2474 * The special identifier for the node, the empty string, or aStringOrNode 2475 * in the event that aStringOrNode happened to already be a string. 2476 */ 2477 _getSpecialIdForNode(aStringOrNode) { 2478 if (typeof aStringOrNode == "object" && aStringOrNode.localName) { 2479 if (aStringOrNode.id) { 2480 return aStringOrNode.id; 2481 } 2482 if (aStringOrNode.localName.startsWith("toolbar")) { 2483 return aStringOrNode.localName.substring(7); 2484 } 2485 return ""; 2486 } 2487 return aStringOrNode; 2488 }, 2489 2490 /** 2491 * Returns true if the passed in ID or node happens to be one of the "special" 2492 * widget types (a separator, a spring, or a spacer). 2493 * 2494 * @param {string|Element} aStringOrNode 2495 * An ID for a node, or an actual node itself to check for special-ness. 2496 * @returns {boolean} 2497 * True if the ID or node resolves to a "special" widget type. 2498 */ 2499 isSpecialWidget(aStringOrNode) { 2500 if (aStringOrNode === null) { 2501 lazy.log.debug("isSpecialWidget was passed null"); 2502 return false; 2503 } 2504 aStringOrNode = this._getSpecialIdForNode(aStringOrNode); 2505 return ( 2506 aStringOrNode.startsWith(kSpecialWidgetPfx) || 2507 aStringOrNode.startsWith("separator") || 2508 aStringOrNode.startsWith("spring") || 2509 aStringOrNode.startsWith("spacer") 2510 ); 2511 }, 2512 2513 /** 2514 * Returns true if the passed in strings (or nodes) happen to be the same 2515 * special widget. 2516 * 2517 * @param {string|Element} aId1 2518 * The first ID or element to compare to the second. 2519 * @param {string|Element} aId2 2520 * The second ID or element to compare to the first. 2521 * @returns {boolean} 2522 * True if the two strings or elements being compared refer to the same 2523 * special widget. 2524 */ 2525 matchingSpecials(aId1, aId2) { 2526 aId1 = this._getSpecialIdForNode(aId1); 2527 aId2 = this._getSpecialIdForNode(aId2); 2528 2529 return ( 2530 this.isSpecialWidget(aId1) && 2531 this.isSpecialWidget(aId2) && 2532 aId1.match(/spring|spacer|separator/)[0] == 2533 aId2.match(/spring|spacer|separator/)[0] 2534 ); 2535 }, 2536 2537 /** 2538 * If the aId string starts with any of "spring", "spacer" or "separator" (any 2539 * of the special widget types), this will add a the special widget prefix 2540 * to the id, as well as a unique numeric suffix at the end and return it. 2541 * 2542 * Otherwise, this simply echoes back the aId string. 2543 * 2544 * @param {string} aId 2545 * @returns {string} 2546 * The aId string with the special widget prefix and unique numeric suffix 2547 * if the aId string is for a special widget, otherwise this just echoes 2548 * back aId. 2549 */ 2550 ensureSpecialWidgetId(aId) { 2551 let nodeType = aId.match(/spring|spacer|separator/)[0]; 2552 // If the ID we were passed isn't a generated one, generate one now: 2553 if (nodeType == aId) { 2554 // Ids are differentiated through a unique count suffix. 2555 return kSpecialWidgetPfx + aId + ++gNewElementCount; 2556 } 2557 return aId; 2558 }, 2559 2560 /** 2561 * @see CustomizableUI.createSpecialWidget 2562 * @param {string} aId 2563 * @param {Document} aDocument 2564 * @returns {Element} 2565 */ 2566 createSpecialWidget(aId, aDocument) { 2567 let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; 2568 let node = aDocument.createXULElement(nodeName); 2569 node.className = "chromeclass-toolbar-additional"; 2570 node.id = this.ensureSpecialWidgetId(aId); 2571 return node; 2572 }, 2573 2574 /** 2575 * Find a XUL-provided widget node in a window. Don't try to use this 2576 * for an API-provided widget or a special widget. 2577 * 2578 * @param {string} aId 2579 * The ID of the XUL-provided widget to find the node for in aWindow. 2580 * @param {DOMWindow} aWindow 2581 * The window to find the XUL-provided widget node for. 2582 * @returns {Element|null} 2583 * The found XUL widget node, or null if it cannot be found. 2584 * @throws {Error} 2585 * Throws if aWindow is not a registered build window. 2586 */ 2587 findXULWidgetInWindow(aId, aWindow) { 2588 if (!gBuildWindows.has(aWindow)) { 2589 throw new Error("Build window not registered"); 2590 } 2591 2592 if (!aId) { 2593 lazy.log.error("findWidgetInWindow was passed an empty string."); 2594 return null; 2595 } 2596 2597 let document = aWindow.document; 2598 2599 // look for a node with the same id, as the node may be 2600 // in a different toolbar. 2601 let node = document.getElementById(aId); 2602 if (node) { 2603 let parent = node.parentNode; 2604 while ( 2605 parent && 2606 !( 2607 this.getCustomizationTarget(parent) || 2608 parent == aWindow.gNavToolbox.palette 2609 ) 2610 ) { 2611 parent = parent.parentNode; 2612 } 2613 2614 if (parent) { 2615 let nodeInArea = 2616 node.parentNode.localName == "toolbarpaletteitem" 2617 ? node.parentNode 2618 : node; 2619 // Check if we're in a customization target, or in the palette: 2620 if ( 2621 (this.getCustomizationTarget(parent) == nodeInArea.parentNode && 2622 gBuildWindows.get(aWindow).has(aWindow.gNavToolbox)) || 2623 aWindow.gNavToolbox.palette == nodeInArea.parentNode 2624 ) { 2625 // Normalize the removable attribute. For backwards compat, if 2626 // the widget is not located in a toolbox palette then absence 2627 // of the "removable" attribute means it is not removable. 2628 if (!node.hasAttribute("removable")) { 2629 // If we first see this in customization mode, it may be in the 2630 // customization palette instead of the toolbox palette. 2631 node.setAttribute( 2632 "removable", 2633 !this.getCustomizationTarget(parent) 2634 ); 2635 } 2636 return node; 2637 } 2638 } 2639 } 2640 2641 let toolboxes = gBuildWindows.get(aWindow); 2642 for (let toolbox of toolboxes) { 2643 if (toolbox.palette) { 2644 // Attempt to locate an element with a matching ID within 2645 // the palette. 2646 let element = toolbox.palette.getElementsByAttribute("id", aId)[0]; 2647 if (element) { 2648 // Normalize the removable attribute. For backwards compat, this 2649 // is optional if the widget is located in the toolbox palette, 2650 // and defaults to *true*, unlike if it was located elsewhere. 2651 if (!element.hasAttribute("removable")) { 2652 element.setAttribute("removable", true); 2653 } 2654 return element; 2655 } 2656 } 2657 } 2658 return null; 2659 }, 2660 2661 /** 2662 * Constructs a node for a customizable UI widget that can be placed within 2663 * aDocument. 2664 * 2665 * @param {Document} aDocument 2666 * The document that the node will be inserted into. 2667 * @param {string|WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget 2668 * The customizable UI widget wrapper or widget ID of the widget to 2669 * construct. 2670 * @returns {Element|null} 2671 * Returns the constructed node for the widget to be placed in aDocument. 2672 * Will return null if the widget node is not allowed to be placed in 2673 * aDocument (for example, if aDocument is a private browsing window 2674 * document, and the widget is not allowed to be placed in such a window). 2675 * @throws {Error} 2676 * Can throw if the document is not a browser window document, or if 2677 * aWidget is null. 2678 */ 2679 buildWidgetNode(aDocument, aWidget) { 2680 if (aDocument.documentURI != kExpectedWindowURL) { 2681 throw new Error("buildWidget was called for a non-browser window!"); 2682 } 2683 if (typeof aWidget == "string") { 2684 aWidget = gPalette.get(aWidget); 2685 } 2686 if (!aWidget) { 2687 throw new Error("buildWidget was passed a non-widget to build."); 2688 } 2689 if ( 2690 !aWidget.showInPrivateBrowsing && 2691 lazy.PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView) 2692 ) { 2693 return null; 2694 } 2695 if ( 2696 aWidget.hideInNonPrivateBrowsing && 2697 !lazy.PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView) 2698 ) { 2699 return null; 2700 } 2701 2702 lazy.log.debug("Building " + aWidget.id + " of type " + aWidget.type); 2703 2704 let node; 2705 let button; 2706 if (aWidget.type == "custom") { 2707 if (aWidget.onBuild) { 2708 node = aWidget.onBuild(aDocument); 2709 } 2710 if ( 2711 !node || 2712 !aDocument.defaultView.XULElement.isInstance(node) || 2713 (aWidget.viewId && !node.viewButton) 2714 ) { 2715 lazy.log.error( 2716 "Custom widget with id " + 2717 aWidget.id + 2718 " does not return a valid node" 2719 ); 2720 } 2721 // A custom widget can define a viewId for the panel and a viewButton 2722 // property for the panel anchor. With that, it will be treated as a view 2723 // type where necessary to hook up the view panel. 2724 if (aWidget.viewId) { 2725 button = node.viewButton; 2726 } 2727 } 2728 // Button and view widget types, plus custom widgets that have a viewId and thus a button. 2729 if (button || aWidget.type != "custom") { 2730 if ( 2731 aWidget.onBeforeCreated && 2732 aWidget.onBeforeCreated(aDocument) === false 2733 ) { 2734 return null; 2735 } 2736 2737 if (!button) { 2738 button = aDocument.createXULElement("toolbarbutton"); 2739 node = button; 2740 } 2741 button.classList.add("toolbarbutton-1"); 2742 button.setAttribute("delegatesanchor", "true"); 2743 2744 let viewbutton = null; 2745 if (aWidget.type == "button-and-view") { 2746 button.setAttribute("id", aWidget.id + "-button"); 2747 let dropmarker = aDocument.createXULElement("toolbarbutton"); 2748 dropmarker.setAttribute("id", aWidget.id + "-dropmarker"); 2749 dropmarker.setAttribute("delegatesanchor", "true"); 2750 dropmarker.classList.add( 2751 "toolbarbutton-1", 2752 "toolbarbutton-combined-buttons-dropmarker" 2753 ); 2754 node = aDocument.createXULElement("toolbaritem"); 2755 node.classList.add("toolbaritem-combined-buttons"); 2756 node.append(button, dropmarker); 2757 viewbutton = dropmarker; 2758 } else if (aWidget.viewId) { 2759 // Also set viewbutton for anything with a view 2760 viewbutton = button; 2761 } 2762 2763 node.setAttribute("id", aWidget.id); 2764 node.setAttribute("widget-id", aWidget.id); 2765 node.setAttribute("widget-type", aWidget.type); 2766 node.toggleAttribute("disabled", !!aWidget.disabled); 2767 node.setAttribute("removable", aWidget.removable); 2768 node.setAttribute("overflows", aWidget.overflows); 2769 if (aWidget.tabSpecific) { 2770 node.setAttribute("tabspecific", aWidget.tabSpecific); 2771 } 2772 if (aWidget.locationSpecific) { 2773 node.setAttribute("locationspecific", aWidget.locationSpecific); 2774 } 2775 if (aWidget.keepBroadcastAttributesWhenCustomizing) { 2776 node.setAttribute( 2777 "keepbroadcastattributeswhencustomizing", 2778 aWidget.keepBroadcastAttributesWhenCustomizing 2779 ); 2780 } 2781 2782 let shortcut; 2783 if (aWidget.shortcutId) { 2784 let keyEl = aDocument.getElementById(aWidget.shortcutId); 2785 if (keyEl) { 2786 shortcut = lazy.ShortcutUtils.prettifyShortcut(keyEl); 2787 } else { 2788 lazy.log.error( 2789 "Key element with id '" + 2790 aWidget.shortcutId + 2791 "' for widget '" + 2792 aWidget.id + 2793 "' not found!" 2794 ); 2795 } 2796 } 2797 2798 if (aWidget.l10nId) { 2799 aDocument.l10n.setAttributes(node, aWidget.l10nId); 2800 if (button != node) { 2801 // This is probably a "button-and-view" widget, such as the Profiler 2802 // button. In that case, "node" is the "toolbaritem" container, and 2803 // "button" the main button (see above). 2804 // In this case, the values on the "node" is used in the Customize 2805 // view, as well as the tooltips over both buttons; the values on the 2806 // "button" are used in the overflow menu. 2807 aDocument.l10n.setAttributes(button, aWidget.l10nId); 2808 } 2809 2810 if (shortcut) { 2811 node.setAttribute("data-l10n-args", JSON.stringify({ shortcut })); 2812 if (button != node) { 2813 // This is probably a "button-and-view" widget. 2814 button.setAttribute("data-l10n-args", JSON.stringify({ shortcut })); 2815 } 2816 } 2817 } else { 2818 node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); 2819 if (button != node) { 2820 // This is probably a "button-and-view" widget. 2821 button.setAttribute("label", node.getAttribute("label")); 2822 } 2823 2824 let tooltip = this.getLocalizedProperty( 2825 aWidget, 2826 "tooltiptext", 2827 shortcut ? [shortcut] : [] 2828 ); 2829 if (tooltip) { 2830 node.setAttribute("tooltiptext", tooltip); 2831 if (button != node) { 2832 // This is probably a "button-and-view" widget. 2833 button.setAttribute("tooltiptext", tooltip); 2834 } 2835 } 2836 } 2837 2838 let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); 2839 node.addEventListener("command", commandHandler); 2840 let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); 2841 node.addEventListener("click", clickHandler); 2842 2843 node.classList.add("chromeclass-toolbar-additional"); 2844 2845 // If the widget has a view, register a keypress handler because opening 2846 // a view with the keyboard has slightly different focus handling than 2847 // opening a view with the mouse. (When opened with the keyboard, the 2848 // first item in the view should be focused after opening.) 2849 if (viewbutton) { 2850 lazy.log.debug( 2851 "Widget " + 2852 aWidget.id + 2853 " has a view. Auto-registering event handlers." 2854 ); 2855 2856 if (aWidget.source == CustomizableUI.SOURCE_BUILTIN) { 2857 node.classList.add("subviewbutton-nav"); 2858 } 2859 } 2860 2861 if (aWidget.onCreated) { 2862 aWidget.onCreated(node); 2863 } 2864 } 2865 2866 aWidget.instances.set(aDocument, node); 2867 return node; 2868 }, 2869 2870 /** 2871 * @see CustomizableUI.ensureSubviewListeners 2872 * @param {Element} viewNode 2873 */ 2874 ensureSubviewListeners(viewNode) { 2875 if (viewNode._addedEventListeners) { 2876 return; 2877 } 2878 let viewId = viewNode.id; 2879 let widget = [...gPalette.values()].find(w => w.viewId == viewId); 2880 if (!widget) { 2881 return; 2882 } 2883 for (let eventName of kSubviewEvents) { 2884 let handler = "on" + eventName; 2885 if (typeof widget[handler] == "function") { 2886 viewNode.addEventListener(eventName, widget[handler]); 2887 } 2888 } 2889 viewNode._addedEventListeners = true; 2890 lazy.log.debug( 2891 "Widget " + widget.id + " showing and hiding event handlers set." 2892 ); 2893 }, 2894 2895 /** 2896 * @see CustomizableUI.getLocalizedProperty 2897 * @param {string|object} aWidget 2898 * @param {string} aProp 2899 * @param {string[]} [aFormatArgs] 2900 * @param {string} [aDef] 2901 * @returns {string} 2902 */ 2903 getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) { 2904 const kReqStringProps = ["label"]; 2905 2906 if (typeof aWidget == "string") { 2907 aWidget = gPalette.get(aWidget); 2908 } 2909 if (!aWidget) { 2910 throw new Error( 2911 "getLocalizedProperty was passed a non-widget to work with." 2912 ); 2913 } 2914 let def, name; 2915 // Let widgets pass their own string identifiers or strings, so that 2916 // we can use strings which aren't the default (in case string ids change) 2917 // and so that non-builtin-widgets can also provide labels, tooltips, etc. 2918 if (aWidget[aProp] != null) { 2919 name = aWidget[aProp]; 2920 // By using this as the default, if a widget provides a full string rather 2921 // than a string ID for localization, we will fall back to that string 2922 // and return that. 2923 def = aDef || name; 2924 } else { 2925 name = aWidget.id + "." + aProp; 2926 def = aDef || ""; 2927 } 2928 if (aWidget.localized === false) { 2929 return def; 2930 } 2931 try { 2932 if (Array.isArray(aFormatArgs) && aFormatArgs.length) { 2933 return ( 2934 lazy.gWidgetsBundle.formatStringFromName(name, aFormatArgs) || def 2935 ); 2936 } 2937 return lazy.gWidgetsBundle.GetStringFromName(name) || def; 2938 } catch (ex) { 2939 // If an empty string was explicitly passed, treat it as an actual 2940 // value rather than a missing property. 2941 if (!def && (name != "" || kReqStringProps.includes(aProp))) { 2942 lazy.log.error("Could not localize property '" + name + "'."); 2943 } 2944 } 2945 return def; 2946 }, 2947 2948 /** 2949 * @see CustomizableUI.addShortcut 2950 * @param {Element} aShortcutNode 2951 * @param {Element|null} aTargetNode 2952 */ 2953 addShortcut(aShortcutNode, aTargetNode = aShortcutNode) { 2954 // Detect if we've already been here before. 2955 if (aTargetNode.hasAttribute("shortcut")) { 2956 return; 2957 } 2958 2959 // Use ownerGlobal.document to ensure we get the right doc even for 2960 // elements in template tags. 2961 let { document } = aShortcutNode.ownerGlobal; 2962 let shortcutId = aShortcutNode.getAttribute("key"); 2963 let shortcut; 2964 if (shortcutId) { 2965 shortcut = document.getElementById(shortcutId); 2966 } else { 2967 let commandId = aShortcutNode.getAttribute("command"); 2968 if (commandId) { 2969 shortcut = lazy.ShortcutUtils.findShortcut( 2970 document.getElementById(commandId) 2971 ); 2972 } 2973 } 2974 if (!shortcut) { 2975 return; 2976 } 2977 2978 aTargetNode.setAttribute( 2979 "shortcut", 2980 lazy.ShortcutUtils.prettifyShortcut(shortcut) 2981 ); 2982 }, 2983 2984 /** 2985 * Executes a customizable UI widget's command handler to handle aEvent. 2986 * 2987 * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget 2988 * The customizable UI widget to call the command handler for. 2989 * @param {Element} aNode 2990 * The node that the aEvent command event fired on. 2991 * @param {CommandEvent} aEvent 2992 * The command event to handle. 2993 */ 2994 doWidgetCommand(aWidget, aNode, aEvent) { 2995 if (aWidget.onCommand) { 2996 try { 2997 aWidget.onCommand.call(null, aEvent); 2998 } catch (e) { 2999 lazy.log.error(e); 3000 } 3001 } else { 3002 // XXXunf Need to think this through more, and formalize. 3003 Services.obs.notifyObservers( 3004 aNode, 3005 "customizedui-widget-command", 3006 aWidget.id 3007 ); 3008 } 3009 }, 3010 3011 /** 3012 * Handles an event on a customizable UI widget node that has a panelview 3013 * associated with it. This routine will cause the panelview to be displayed. 3014 * 3015 * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget 3016 * The customizable UI widget to show the panelview for. 3017 * @param {Element} aNode 3018 * The node that is handling the event that is causing the panelview to 3019 * show. 3020 * @param {Event} aEvent 3021 * The event that the node is handlign that is causing the panelview to 3022 * show. 3023 */ 3024 showWidgetView(aWidget, aNode, aEvent) { 3025 let ownerWindow = aNode.ownerGlobal; 3026 let area = this.getPlacementOfWidget(aNode.id).area; 3027 let areaType = CustomizableUI.getAreaType(area); 3028 let anchor = aNode; 3029 3030 if ( 3031 aWidget.disallowSubView && 3032 (areaType == CustomizableUI.TYPE_PANEL || 3033 aNode.hasAttribute("overflowedItem")) 3034 ) { 3035 // Close the containing panel (e.g. overflow), PanelUI will reopen. 3036 let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); 3037 if (wrapper?.anchor) { 3038 this.hidePanelForNode(aNode); 3039 anchor = wrapper.anchor; 3040 } 3041 } else if (areaType != CustomizableUI.TYPE_PANEL) { 3042 let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); 3043 3044 let hasMultiView = !!aNode.closest("panelmultiview"); 3045 if (!hasMultiView && wrapper?.anchor) { 3046 this.hidePanelForNode(aNode); 3047 anchor = wrapper.anchor; 3048 } 3049 } 3050 ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, aEvent); 3051 }, 3052 3053 /** 3054 * Handles a command event on a customizable ui widget node, and does the 3055 * action that best suits the type of widget. 3056 * 3057 * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget 3058 * The widget to handle the command event for. 3059 * @param {Element} aNode 3060 * The node that the command event is being fired on. 3061 * @param {CommandEvent} aEvent 3062 * The command event to be handled. 3063 */ 3064 handleWidgetCommand(aWidget, aNode, aEvent) { 3065 // Note that aEvent can be a keypress event for widgets of type "view". 3066 lazy.log.debug("handleWidgetCommand"); 3067 3068 let action; 3069 if (aWidget.onBeforeCommand) { 3070 try { 3071 action = aWidget.onBeforeCommand.call(null, aEvent, aNode); 3072 } catch (e) { 3073 lazy.log.error(e); 3074 } 3075 } 3076 3077 if (aWidget.type == "button" || action == "command") { 3078 this.doWidgetCommand(aWidget, aNode, aEvent); 3079 } else if (aWidget.type == "view" || action == "view") { 3080 this.showWidgetView(aWidget, aNode, aEvent); 3081 } else if (aWidget.type == "button-and-view") { 3082 // Do the command if we're in the toolbar and the button was clicked. 3083 // Otherwise, including when we have currently overflowed out of the 3084 // toolbar, open the view. There is no way to trigger the command while 3085 // the widget is in the panel, by design. 3086 let button = aNode.firstElementChild; 3087 let area = this.getPlacementOfWidget(aNode.id).area; 3088 let areaType = CustomizableUI.getAreaType(area); 3089 if ( 3090 areaType == CustomizableUI.TYPE_TOOLBAR && 3091 button.contains(aEvent.target) && 3092 !aNode.hasAttribute("overflowedItem") 3093 ) { 3094 this.doWidgetCommand(aWidget, aNode, aEvent); 3095 } else { 3096 this.showWidgetView(aWidget, aNode, aEvent); 3097 } 3098 } 3099 }, 3100 3101 /** 3102 * Handles a click event on a customizable ui widget node, and redirects to 3103 * the widgets onClick event handler, if such a handler exists. 3104 * 3105 * @param {WidgetGroupWrapper|XULWidgetGroupWrapper} aWidget 3106 * The widget to call the onClick event handler for if such a handler 3107 * exists. If the handler does not exist, nothing is called. 3108 * @param {Element} aNode 3109 * The node that fired the click event. 3110 * @param {MouseEvent} aEvent 3111 * The click event to be handled. 3112 */ 3113 handleWidgetClick(aWidget, aNode, aEvent) { 3114 lazy.log.debug("handleWidgetClick"); 3115 if (aWidget.onClick) { 3116 try { 3117 aWidget.onClick.call(null, aEvent); 3118 } catch (e) { 3119 console.error(e); 3120 } 3121 } else { 3122 // XXXunf Need to think this through more, and formalize. 3123 Services.obs.notifyObservers( 3124 aNode, 3125 "customizedui-widget-click", 3126 aWidget.id 3127 ); 3128 } 3129 }, 3130 3131 /** 3132 * Returns the closest <xul:panel> element to aNode in its ancestry, or null 3133 * if no such node can be found. 3134 * 3135 * @param {Element} aNode 3136 * The node to check the ancestry for. 3137 * @returns {Element|null} 3138 */ 3139 _getPanelForNode(aNode) { 3140 return aNode.closest("panel"); 3141 }, 3142 3143 /** 3144 * If people put things in the panel which need more than single-click 3145 * interaction, we don't want to close it. Right now we check for text inputs 3146 * and menu buttons. We also check for being outside of any 3147 * toolbaritem/toolbarbutton, ie on a blank part of the menu, or on another 3148 * menu (like a context menu inside the panel). 3149 * 3150 * So this returns true if the event being handled is on one of these 3151 * interactive things that should NOT result in the associated panel closing. 3152 * 3153 * @param {Event} aEvent 3154 * The event that is occurring that we're evaluating. 3155 * @returns {boolean} 3156 * True if the event occurred on an item we consider "interactive" such 3157 * that the enclosing panel should remain open. False if the panel should 3158 * close. 3159 */ 3160 _isOnInteractiveElement(aEvent) { 3161 let panel = this._getPanelForNode(aEvent.currentTarget); 3162 // This can happen in e.g. customize mode. If there's no panel, 3163 // there's clearly nothing for us to close; pretend we're interactive. 3164 if (!panel) { 3165 return true; 3166 } 3167 3168 function getNextTarget(target) { 3169 if (target.nodeType == target.DOCUMENT_NODE) { 3170 if (!target.defaultView) { 3171 // Err, we're done. 3172 return null; 3173 } 3174 // Find containing browser or iframe element in the parent doc. 3175 return target.defaultView.docShell.chromeEventHandler; 3176 } 3177 // Skip any parent shadow roots 3178 return target.parentNode?.host?.parentNode || target.parentNode; 3179 } 3180 3181 // While keeping track of that, we go from the original target back up, 3182 // to the panel if we have to. We bail as soon as we find an input, 3183 // a toolbarbutton/item, or a menuItem. 3184 for ( 3185 let target = aEvent.originalTarget; 3186 target && target != panel; 3187 target = getNextTarget(target) 3188 ) { 3189 if (target.nodeType == target.DOCUMENT_NODE) { 3190 // Skip out of iframes etc: 3191 continue; 3192 } 3193 3194 // Skip out of shadow roots 3195 if ( 3196 target.nodeType == target.DOCUMENT_FRAGMENT_NODE && 3197 target.containingShadowRoot == target 3198 ) { 3199 continue; 3200 } 3201 3202 // Break out of the loop immediately for disabled items, as we need to 3203 // keep the menu open in that case. 3204 if (target.getAttribute("disabled") == "true") { 3205 return true; 3206 } 3207 3208 let tagName = target.localName; 3209 if (tagName == "input" || tagName == "searchbar") { 3210 return true; 3211 } 3212 if (tagName == "toolbaritem" || tagName == "toolbarbutton") { 3213 // If we are in a type="menu" toolbarbutton, we'll now interact with 3214 // the menu. 3215 return target.getAttribute("type") == "menu"; 3216 } 3217 if (tagName == "menuitem") { 3218 // If we're in a nested menu we don't need to close this panel. 3219 return true; 3220 } 3221 } 3222 3223 // We don't know what we interacted with, assume interactive. 3224 return true; 3225 }, 3226 3227 /** 3228 * Finds the associated panel (if any) enclosing a given element, and 3229 * closes it. 3230 * 3231 * @param {Element} aNode 3232 * The node to close the panel ancestor for. 3233 */ 3234 hidePanelForNode(aNode) { 3235 let panel = this._getPanelForNode(aNode); 3236 if (panel) { 3237 lazy.PanelMultiView.hidePopup(panel); 3238 } 3239 }, 3240 3241 /** 3242 * For an event that occurs for a node within a panel, this routine will 3243 * check to see if that event should cause the panel to close. 3244 * 3245 * @param {Event} aEvent 3246 * The event that is being fired on the node. This could be a keyboard, 3247 * mouse or command event, for example. 3248 */ 3249 maybeAutoHidePanel(aEvent) { 3250 let eventType = aEvent.type; 3251 if (eventType == "keypress" && aEvent.keyCode != aEvent.DOM_VK_RETURN) { 3252 return; 3253 } 3254 3255 if (eventType == "click" && aEvent.button != 0) { 3256 return; 3257 } 3258 3259 // We don't check preventDefault - it makes sense that this was prevented, 3260 // but we probably still want to close the panel. If consumers don't want 3261 // this to happen, they should specify the closemenu attribute. 3262 if (eventType != "command" && this._isOnInteractiveElement(aEvent)) { 3263 return; 3264 } 3265 3266 // We can't use event.target because we might have passed an anonymous 3267 // content boundary as well, and so target points to the outer element in 3268 // that case. Unfortunately, this means we get anonymous child nodes instead 3269 // of the real ones, so looking for the 'stoooop, don't close me' attributes 3270 // is more involved. 3271 let target = aEvent.originalTarget; 3272 while (target.parentNode && target.localName != "panel") { 3273 if ( 3274 target.getAttribute("closemenu") == "none" || 3275 target.getAttribute("widget-type") == "view" || 3276 target.getAttribute("widget-type") == "button-and-view" || 3277 target.hasAttribute("view-button-id") 3278 ) { 3279 return; 3280 } 3281 3282 target = target.parentNode; 3283 3284 // If we reached the shadow boundry, let's cross it while we head up 3285 // the tree. 3286 if (ShadowRoot.isInstance(target)) { 3287 target = target.host; 3288 } 3289 } 3290 3291 // If we get here, we can actually hide the popup: 3292 this.hidePanelForNode(aEvent.target); 3293 }, 3294 3295 /** 3296 * @see CustomizableUI.getUnusedWidgets 3297 * @param {DOMElement} aWindowPalette 3298 * @returns {Array<WidgetGroupWrapper|XULWidgetGroupWrapper>} 3299 */ 3300 getUnusedWidgets(aWindowPalette) { 3301 let window = aWindowPalette.ownerGlobal; 3302 let isWindowPrivate = lazy.PrivateBrowsingUtils.isWindowPrivate(window); 3303 // We use a Set because there can be overlap between the widgets in 3304 // gPalette and the items in the palette, especially after the first 3305 // customization, since programmatically generated widgets will remain 3306 // in the toolbox palette. 3307 let widgets = new Set(); 3308 3309 // It's possible that some widgets have been defined programmatically and 3310 // have not been overlayed into the palette. We can find those inside 3311 // gPalette. 3312 for (let [id, widget] of gPalette) { 3313 if (!widget.currentArea) { 3314 if ( 3315 (isWindowPrivate && widget.showInPrivateBrowsing) || 3316 (!isWindowPrivate && !widget.hideInNonPrivateBrowsing) 3317 ) { 3318 widgets.add(id); 3319 } 3320 } 3321 } 3322 3323 lazy.log.debug("Iterating the actual nodes of the window palette"); 3324 for (let node of aWindowPalette.children) { 3325 lazy.log.debug("In palette children: " + node.id); 3326 if (node.id && !this.getPlacementOfWidget(node.id)) { 3327 widgets.add(node.id); 3328 } 3329 } 3330 3331 return [...widgets]; 3332 }, 3333 3334 /** 3335 * @see CustomizableUI.getPlacementOfWidget 3336 * @param {string} aWidgetId 3337 * @param {boolean} [aOnlyRegistered=true] 3338 * @param {boolean} [aDeadAreas=false] 3339 * @returns {CustomizableUIPlacementInfo|null} 3340 */ 3341 getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas) { 3342 if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { 3343 return null; 3344 } 3345 3346 for (let [area, placements] of gPlacements) { 3347 if (!gAreas.has(area) && !aDeadAreas) { 3348 continue; 3349 } 3350 let index = placements.indexOf(aWidgetId); 3351 if (index != -1) { 3352 return { area, position: index }; 3353 } 3354 } 3355 3356 return null; 3357 }, 3358 3359 /** 3360 * Check for the current existance of a widget by ID. 3361 * 3362 * @see CustomizableUIInternal.isSpecialWidget 3363 * @param {string} aWidgetId 3364 * @returns {boolean} 3365 * Returns true if the widget ID belongs to a widget that is registered or 3366 * is a "special" widget (see isSpecialWidget). This will return false for 3367 * widget IDs belonging to widgets we have seen in the past, but are no 3368 * longer registered. 3369 */ 3370 widgetExists(aWidgetId) { 3371 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { 3372 return true; 3373 } 3374 3375 // Destroyed API widgets are in gSeenWidgets, but not in gPalette: 3376 // If it's not in gPalette, it doesn't exist. 3377 if (gSeenWidgets.has(aWidgetId)) { 3378 return false; 3379 } 3380 3381 // We're assuming XUL widgets always exist, as it's much harder to check, 3382 // and checking would be much more error prone. 3383 return true; 3384 }, 3385 3386 /** 3387 * @see CustomizableUI.addWidgetToArea 3388 * @param {string} aWidgetId 3389 * @param {string} aArea 3390 * @param {number} aPosition 3391 * @param {boolean} [aInitialAdd=false] 3392 * True if this is the first time the widget is being added to the area 3393 * for the first time during initialization, or after a state reset. 3394 */ 3395 addWidgetToArea(aWidgetId, aArea, aPosition, aInitialAdd = false) { 3396 if (aArea == CustomizableUI.AREA_NO_AREA) { 3397 throw new Error( 3398 "AREA_NO_AREA is only used as an argument for " + 3399 "canWidgetMoveToArea. Use removeWidgetFromArea instead." 3400 ); 3401 } 3402 if (!gAreas.has(aArea)) { 3403 throw new Error("Unknown customization area: " + aArea); 3404 } 3405 3406 // Hack: don't want special widgets in the panel (need to check here as well 3407 // as in canWidgetMoveToArea because the menu panel is lazy): 3408 if ( 3409 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL && 3410 this.isSpecialWidget(aWidgetId) 3411 ) { 3412 return; 3413 } 3414 3415 // If this is a lazy area that hasn't been restored yet, we can't yet modify 3416 // it - would would at least like to add to it. So we keep track of it in 3417 // gFuturePlacements, and use that to add it when restoring the area. We 3418 // throw away aPosition though, as that can only be bogus if the area hasn't 3419 // yet been restorted (caller can't possibly know where its putting the 3420 // widget in relation to other widgets). 3421 if (this.isAreaLazy(aArea)) { 3422 gFuturePlacements.get(aArea).add(aWidgetId); 3423 return; 3424 } 3425 3426 if (this.isSpecialWidget(aWidgetId)) { 3427 aWidgetId = this.ensureSpecialWidgetId(aWidgetId); 3428 } 3429 3430 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); 3431 if (oldPlacement && oldPlacement.area == aArea) { 3432 this.moveWidgetWithinArea(aWidgetId, aPosition); 3433 return; 3434 } 3435 3436 // Do nothing if the widget is not allowed to move to the target area. 3437 if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { 3438 return; 3439 } 3440 3441 if (oldPlacement) { 3442 this.removeWidgetFromArea(aWidgetId); 3443 } 3444 3445 if (!gPlacements.has(aArea)) { 3446 gPlacements.set(aArea, [aWidgetId]); 3447 aPosition = 0; 3448 } else { 3449 let placements = gPlacements.get(aArea); 3450 if (typeof aPosition != "number") { 3451 aPosition = placements.length; 3452 } 3453 if (aPosition < 0) { 3454 aPosition = 0; 3455 } 3456 placements.splice(aPosition, 0, aWidgetId); 3457 } 3458 3459 let widget = gPalette.get(aWidgetId); 3460 if (widget) { 3461 widget.currentArea = aArea; 3462 widget.currentPosition = aPosition; 3463 } 3464 3465 // We initially set placements with addWidgetToArea, so in that case 3466 // we don't consider the area "dirtied". 3467 if (!aInitialAdd) { 3468 gDirtyAreaCache.add(aArea); 3469 } 3470 3471 gDirty = true; 3472 this.saveState(); 3473 3474 this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); 3475 }, 3476 3477 /** 3478 * @see CustomizableUI.removeWidgetFromArea 3479 * @param {string} aWidgetId 3480 */ 3481 removeWidgetFromArea(aWidgetId) { 3482 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); 3483 if (!oldPlacement) { 3484 return; 3485 } 3486 3487 if (!this.isWidgetRemovable(aWidgetId)) { 3488 return; 3489 } 3490 3491 let placements = gPlacements.get(oldPlacement.area); 3492 let position = placements.indexOf(aWidgetId); 3493 if (position != -1) { 3494 placements.splice(position, 1); 3495 } 3496 3497 let widget = gPalette.get(aWidgetId); 3498 if (widget) { 3499 widget.currentArea = null; 3500 widget.currentPosition = null; 3501 } 3502 3503 gDirty = true; 3504 this.saveState(); 3505 gDirtyAreaCache.add(oldPlacement.area); 3506 3507 // If we're in vertical tabs, ensure we don't restore the widget when we toggle back 3508 // to horizontal tabs. 3509 if (!gInBatchStack && CustomizableUI.verticalTabsEnabled) { 3510 if (oldPlacement.area == CustomizableUI.AREA_TABSTRIP) { 3511 this.deleteWidgetInSavedHorizontalTabStripState(aWidgetId); 3512 } else if ( 3513 oldPlacement.area == CustomizableUI.AREA_NAVBAR && 3514 this.getSavedHorizontalSnapshotState().includes(aWidgetId) 3515 ) { 3516 this.deleteWidgetInSavedHorizontalTabStripState(aWidgetId); 3517 this.deleteWidgetInSavedNavBarWhenVerticalTabsState(aWidgetId); 3518 } 3519 } 3520 3521 this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); 3522 }, 3523 3524 /** 3525 * @see CustomizableUI.moveWidgetWithinArea 3526 * @param {string} aWidgetId 3527 * @param {number} aPosition 3528 */ 3529 moveWidgetWithinArea(aWidgetId, aPosition) { 3530 let oldPlacement = this.getPlacementOfWidget(aWidgetId); 3531 if (!oldPlacement) { 3532 return; 3533 } 3534 3535 let placements = gPlacements.get(oldPlacement.area); 3536 if (typeof aPosition != "number") { 3537 aPosition = placements.length; 3538 } else if (aPosition < 0) { 3539 aPosition = 0; 3540 } else if (aPosition > placements.length) { 3541 aPosition = placements.length; 3542 } 3543 3544 let widget = gPalette.get(aWidgetId); 3545 if (widget) { 3546 widget.currentPosition = aPosition; 3547 widget.currentArea = oldPlacement.area; 3548 } 3549 3550 if (aPosition == oldPlacement.position) { 3551 return; 3552 } 3553 3554 placements.splice(oldPlacement.position, 1); 3555 // If we just removed the item from *before* where it is now added, 3556 // we need to compensate the position offset for that: 3557 if (oldPlacement.position < aPosition) { 3558 aPosition--; 3559 } 3560 placements.splice(aPosition, 0, aWidgetId); 3561 3562 gDirty = true; 3563 gDirtyAreaCache.add(oldPlacement.area); 3564 3565 this.saveState(); 3566 3567 this.notifyListeners( 3568 "onWidgetMoved", 3569 aWidgetId, 3570 oldPlacement.area, 3571 oldPlacement.position, 3572 aPosition 3573 ); 3574 }, 3575 3576 /** 3577 * Returns the horizontal tab strip's placements state that was saved the 3578 * last time we switched to vertical tabs mode. This state is an array of 3579 * widget IDs that had been in the tab strip prior to switching to vertical 3580 * tabs. 3581 * 3582 * @returns {string[]} 3583 */ 3584 getSavedHorizontalSnapshotState() { 3585 let state = []; 3586 let prefValue = lazy.horizontalPlacementsPref; 3587 if (prefValue) { 3588 try { 3589 state = JSON.parse(prefValue); 3590 } catch (e) { 3591 lazy.log.warn( 3592 `Failed to parse value of ${kPrefCustomizationHorizontalTabstrip}`, 3593 e 3594 ); 3595 } 3596 } 3597 return state; 3598 }, 3599 3600 /** 3601 * Returns the vertical tab strip's placements state that was saved the 3602 * last time we switched to horizontal tabs mode. This state is an array of 3603 * widget IDs that had been in the vertical tab strip prior to switching to 3604 * horizontal tabs. 3605 * 3606 * @returns {string[]} 3607 */ 3608 getSavedVerticalSnapshotState() { 3609 let state = []; 3610 let prefValue = lazy.verticalPlacementsPref; 3611 if (prefValue) { 3612 try { 3613 state = JSON.parse(prefValue); 3614 } catch (e) { 3615 lazy.log.warn( 3616 `Failed to parse value of ${kPrefCustomizationNavBarWhenVerticalTabs}`, 3617 e 3618 ); 3619 } 3620 } 3621 return state; 3622 }, 3623 3624 /** 3625 * Loads the saved customization state objects from preferences or sets them 3626 * to their defaults if no such state can be found. 3627 * 3628 * Note that this does not populate gPlacements, which is done lazily. 3629 */ 3630 loadSavedState() { 3631 let state = Services.prefs.getCharPref(kPrefCustomizationState, ""); 3632 if (!state) { 3633 lazy.log.debug("No saved state found"); 3634 // Nothing has been customized, so silently fall back to the defaults. 3635 return; 3636 } 3637 try { 3638 gSavedState = JSON.parse(state); 3639 if (typeof gSavedState != "object" || gSavedState === null) { 3640 throw new Error("Invalid saved state"); 3641 } 3642 } catch (e) { 3643 Services.prefs.clearUserPref(kPrefCustomizationState); 3644 gSavedState = {}; 3645 lazy.log.debug( 3646 "Error loading saved UI customization state, falling back to defaults." 3647 ); 3648 } 3649 3650 if (!("placements" in gSavedState)) { 3651 gSavedState.placements = {}; 3652 } 3653 3654 if (!("currentVersion" in gSavedState)) { 3655 gSavedState.currentVersion = 0; 3656 } 3657 3658 if (!("currentVersionBaseBrowser" in gSavedState)) { 3659 gSavedState.currentVersionBaseBrowser = 0; 3660 } 3661 3662 if (!("currentVersionTorBrowser" in gSavedState)) { 3663 gSavedState.currentVersionTorBrowser = 0; 3664 } 3665 3666 gSeenWidgets = new Set(gSavedState.seen || []); 3667 gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []); 3668 gNewElementCount = gSavedState.newElementCount || 0; 3669 }, 3670 3671 /** 3672 * Restores the placements of widgets within an area with ID aAreaId from 3673 * saved state, or sets the default placements if no such saved state exists. 3674 * This should be called during area registration, or after a state reset. 3675 * 3676 * @param {string} aAreaId 3677 * The ID of the area to restore the state for. 3678 */ 3679 restoreStateForArea(aAreaId) { 3680 let placementsPreexisted = gPlacements.has(aAreaId); 3681 3682 this.beginBatchUpdate(); 3683 try { 3684 gRestoring = true; 3685 3686 let restored = false; 3687 if (placementsPreexisted) { 3688 lazy.log.debug( 3689 "Restoring " + aAreaId + " from pre-existing placements" 3690 ); 3691 for (let [position, id] of gPlacements.get(aAreaId).entries()) { 3692 this.moveWidgetWithinArea(id, position); 3693 } 3694 gDirty = false; 3695 restored = true; 3696 } else { 3697 gPlacements.set(aAreaId, []); 3698 } 3699 3700 if (!restored && gSavedState && aAreaId in gSavedState.placements) { 3701 lazy.log.debug("Restoring " + aAreaId + " from saved state"); 3702 let placements = gSavedState.placements[aAreaId]; 3703 for (let id of placements) { 3704 this.addWidgetToArea(id, aAreaId); 3705 } 3706 gDirty = false; 3707 restored = true; 3708 } 3709 3710 if (!restored) { 3711 lazy.log.debug("Restoring " + aAreaId + " from default state"); 3712 let defaults = gAreas.get(aAreaId).get("defaultPlacements"); 3713 if ( 3714 CustomizableUI.verticalTabsEnabled && 3715 gAreas.get(aAreaId).has("verticalTabsDefaultPlacements") 3716 ) { 3717 lazy.log.debug( 3718 "Using verticalTabsDefaultPlacements to restore " + aAreaId 3719 ); 3720 defaults = gAreas.get(aAreaId).get("verticalTabsDefaultPlacements"); 3721 } 3722 3723 if (defaults) { 3724 for (let id of defaults) { 3725 this.addWidgetToArea(id, aAreaId, null, true); 3726 } 3727 } 3728 gDirty = false; 3729 } 3730 3731 // Finally, add widgets to the area that were added before the it was able 3732 // to be restored. This can occur when add-ons register widgets for a 3733 // lazily-restored area before it's been restored. 3734 if (gFuturePlacements.has(aAreaId)) { 3735 let areaPlacements = gPlacements.get(aAreaId); 3736 for (let id of gFuturePlacements.get(aAreaId)) { 3737 if (areaPlacements.includes(id)) { 3738 continue; 3739 } 3740 this.addWidgetToArea(id, aAreaId); 3741 } 3742 gFuturePlacements.delete(aAreaId); 3743 } 3744 3745 lazy.log.debug( 3746 "Placements for " + 3747 aAreaId + 3748 ":\n\t" + 3749 gPlacements.get(aAreaId).join("\n\t") 3750 ); 3751 3752 gRestoring = false; 3753 } finally { 3754 this.endBatchUpdate(); 3755 } 3756 }, 3757 3758 /** 3759 * Adds widgets to the AREA_TABSTRIP area that were saved when switching away 3760 * from horizontal tabs. This will effectively rebuild AREA_TABSTRIP to 3761 * match the state in savedPlacements, and then clear the saved horizontal 3762 * tab state. 3763 * 3764 * @param {string[]} [savedPlacements=this.getSavedHorizontalSnapshotState()] 3765 * The array of widget IDs to add to the AREA_TABSTRIP, in order. If this 3766 * set of placements doesn't include "tabbrowser-tabs" for some reason, the 3767 * whole set of placements is ignored and the tab strip is reset to the 3768 * defaults. 3769 * @param {boolean} [isInitializing = false] 3770 * True if the horizontal tab strip is being rebuilt during CustomizableUI 3771 * initialization as opposed to via a pref-flip at runtime. 3772 */ 3773 restoreSavedHorizontalTabStripState( 3774 savedPlacements = this.getSavedHorizontalSnapshotState(), 3775 isInitializing = false 3776 ) { 3777 const tabstripAreaId = CustomizableUI.AREA_TABSTRIP; 3778 lazy.log.debug( 3779 `restoreSavedHorizontalTabStripState, ${kPrefCustomizationHorizontalTabstrip} contained:`, 3780 savedPlacements 3781 ); 3782 // If there's no saved state, or it doesn't pass the sniff test, use 3783 // default placements instead 3784 if (!savedPlacements.includes("tabbrowser-tabs")) { 3785 savedPlacements = gAreas.get(tabstripAreaId).get("defaultPlacements"); 3786 lazy.log.debug(`Using defaultPlacements for ${tabstripAreaId}`); 3787 } 3788 3789 lazy.log.debug( 3790 `Replacing existing placements: ${gPlacements.get( 3791 tabstripAreaId 3792 )}, with ${savedPlacements}.` 3793 ); 3794 3795 // Restore the tabstrip to either saved or default placements 3796 this.beginBatchUpdate(); 3797 for (let [index, widgetId] of savedPlacements.entries()) { 3798 this.addWidgetToArea(widgetId, tabstripAreaId, index, isInitializing); 3799 } 3800 3801 // Wipe the pref now that state is restored 3802 Services.prefs.clearUserPref(kPrefCustomizationHorizontalTabstrip); 3803 3804 // The vertical tabstrip area is supposed to be empty when we switch back to horizontal 3805 if (gPlacements.get(CustomizableUI.AREA_VERTICAL_TABSTRIP)?.length) { 3806 lazy.log.warn( 3807 `Widgets remain in ${CustomizableUI.AREA_VERTICAL_TABSTRIP}:`, 3808 gPlacements.get(CustomizableUI.AREA_VERTICAL_TABSTRIP) 3809 ); 3810 } 3811 3812 this.endBatchUpdate(); 3813 }, 3814 3815 /** 3816 * Looks for a widget with a matching ID to aWidgetId within the saved 3817 * horizontal tab strip state, and then deletes it from that state before 3818 * saving that state to preferences. 3819 * 3820 * @see CustomizableUIInternal.saveHorizontalTabStripState 3821 * @param {string} aWidgetId 3822 * The ID of the widget to remove from the saved horizontal tabstrip state. 3823 */ 3824 deleteWidgetInSavedHorizontalTabStripState(aWidgetId) { 3825 const savedPlacements = this.getSavedHorizontalSnapshotState(); 3826 let position = savedPlacements.indexOf(aWidgetId); 3827 if (position != -1) { 3828 savedPlacements.splice(position, 1); 3829 this.saveHorizontalTabStripState(savedPlacements); 3830 } 3831 }, 3832 3833 /** 3834 * Looks for a widget with a matching ID to aWidgetId within the navbar state 3835 * that was saved when switching away from vertical tabs mode, and then 3836 * deletes it from that state before saving that state to preferences. 3837 * 3838 * @see CustomizableUIInternal.saveNavBarWhenVerticalTabsState 3839 * @param {string} aWidgetId 3840 * The ID of the widget to remove from the saved navbar state. 3841 */ 3842 deleteWidgetInSavedNavBarWhenVerticalTabsState(aWidgetId) { 3843 const savedPlacements = this.getSavedVerticalSnapshotState(); 3844 let position = savedPlacements.indexOf(aWidgetId); 3845 if (position != -1) { 3846 savedPlacements.splice(position, 1); 3847 this.saveNavBarWhenVerticalTabsState(savedPlacements); 3848 } 3849 }, 3850 3851 /** 3852 * Takes the current set of widgets placed within the horizontal tab strip 3853 * and saves their IDs to a preference. This is used just before switching 3854 * to vertical tabs mode (which moves some widgets around), and the saved 3855 * state is used to restore the horizontal tab mode state of the tab 3856 * strip. 3857 * 3858 * @param {string[]} [placements=[]] 3859 * The placements within the horizontal tab strip to save to preferences. 3860 * If this is the empty array, this method will use the current placements 3861 * of the tab strip automatically. 3862 */ 3863 saveHorizontalTabStripState(placements = []) { 3864 if (!placements.length) { 3865 placements = this.getAreaPlacementsForSaving( 3866 CustomizableUI.AREA_TABSTRIP 3867 ); 3868 } 3869 let serialized = JSON.stringify(placements, this.serializerHelper); 3870 lazy.log.debug("Saving horizontal tabstrip state.", serialized); 3871 Services.prefs.setCharPref( 3872 kPrefCustomizationHorizontalTabstrip, 3873 serialized 3874 ); 3875 }, 3876 3877 /** 3878 * Takes the current set of widgets placed within the navbar while in 3879 * vertical tabs mode, and and saves their IDs to a preference. This is used 3880 * just before switching to horizontal tabs mode (which moves some widgets 3881 * around), and the saved state is used to restore the navbar's widget 3882 * placements if the user switches back to vertical tabs. 3883 * 3884 * @param {string[]} [placements=[]] 3885 * The placements within the navbar to save to preferences. If this is the 3886 * empty array, this method will use the current placements of the navbar 3887 * automatically. 3888 */ 3889 saveNavBarWhenVerticalTabsState(placements = []) { 3890 if (!placements.length) { 3891 placements = this.getAreaPlacementsForSaving(CustomizableUI.AREA_NAVBAR); 3892 } 3893 let serialized = JSON.stringify(placements, this.serializerHelper); 3894 lazy.log.debug("Saving vertical navbar state.", serialized); 3895 Services.prefs.setCharPref( 3896 kPrefCustomizationNavBarWhenVerticalTabs, 3897 serialized 3898 ); 3899 }, 3900 3901 /** 3902 * Returns the placements of widgets within a known area, regardless of 3903 * whether or not the area has already been built, or is still registered. 3904 * This means that we can get the placements for an area that was registered 3905 * in the past, is no longer registered, but still exists within the 3906 * saved state. 3907 * 3908 * @param {string} aAreaId 3909 * The ID of the area to get the placements for. 3910 * @returns {string[]|undefined} 3911 * Returns the placements for the area, or undefined if the area is not 3912 * recognized. 3913 */ 3914 getAreaPlacementsForSaving(aAreaId) { 3915 // An early call to saveState can occur before all the lazy-area building is complete 3916 let placements; 3917 if (this.isAreaLazy(aAreaId) && gFuturePlacements.get(aAreaId)?.size) { 3918 placements = [...gFuturePlacements.get(aAreaId)]; 3919 } else if (gPlacements.has(aAreaId)) { 3920 placements = gPlacements.get(aAreaId); 3921 } 3922 3923 // Merge in previously saved areas if not present in gPlacements/gFuturePlacements. 3924 // This way, state is still persisted for e.g. temporarily disabled 3925 // add-ons - see bug 989338. 3926 if (!placements && gSavedState && gSavedState.placements?.[aAreaId]) { 3927 placements = gSavedState.placements[aAreaId]; 3928 } 3929 lazy.log.debug( 3930 `getAreaPlacementsForSaving for area: ${aAreaId}, gPlacements for area: ${gPlacements.get( 3931 aAreaId 3932 )}, returning: ${placements}` 3933 ); 3934 return placements; 3935 }, 3936 3937 /** 3938 * Saves the current state of all customizable areas to preferences. 3939 */ 3940 saveState() { 3941 if (gInBatchStack || !gDirty) { 3942 return; 3943 } 3944 // Clone because we want to modify this map: 3945 let placements = new Map(); 3946 // Because of Bug 989338 and the risk of having area ids that aren't yet registered, 3947 // we collect the areas from both gPlacements and gSavedState rather than gAreas. 3948 let allAreaIds = new Set([...gPlacements.keys()]); 3949 if (gSavedState?.placements) { 3950 for (let area of Object.keys(gSavedState.placements)) { 3951 allAreaIds.add(area); 3952 } 3953 } 3954 for (let area of allAreaIds) { 3955 placements.set(area, this.getAreaPlacementsForSaving(area)); 3956 } 3957 let state = { 3958 placements, 3959 seen: gSeenWidgets, 3960 dirtyAreaCache: gDirtyAreaCache, 3961 currentVersion: kVersion, 3962 currentVersionBaseBrowser: kVersionBaseBrowser, 3963 currentVersionTorBrowser: kVersionTorBrowser, 3964 newElementCount: gNewElementCount, 3965 }; 3966 3967 lazy.log.debug("Saving state."); 3968 let serialized = JSON.stringify(state, this.serializerHelper); 3969 lazy.log.debug("State saved as: " + serialized); 3970 Services.prefs.setCharPref(kPrefCustomizationState, serialized); 3971 gDirty = false; 3972 }, 3973 3974 /** 3975 * This helper is passed to JSON.stringify when serializing the current 3976 * customizable areas to preferences in saveState(). This does the work 3977 * of serializing Map and Sets to objects and arrays, respectively. 3978 * 3979 * @see CustomizableUIInternal.saveState() 3980 * @param {string|symbol} _aKey 3981 * @param {any} aValue 3982 * @returns {any} 3983 */ 3984 serializerHelper(_aKey, aValue) { 3985 if (typeof aValue == "object" && aValue.constructor.name == "Map") { 3986 let result = {}; 3987 for (let [mapKey, mapValue] of aValue) { 3988 result[mapKey] = mapValue; 3989 } 3990 return result; 3991 } 3992 3993 if (typeof aValue == "object" && aValue.constructor.name == "Set") { 3994 return [...aValue]; 3995 } 3996 3997 return aValue; 3998 }, 3999 4000 /** 4001 * @see CustomizableUI.beginBatchUpdate() 4002 */ 4003 beginBatchUpdate() { 4004 gInBatchStack++; 4005 }, 4006 4007 /** 4008 * @see CustomizableUI.endBatchUpdate() 4009 * @param {boolean} aForceDirty 4010 */ 4011 endBatchUpdate(aForceDirty) { 4012 gInBatchStack--; 4013 if (aForceDirty === true) { 4014 gDirty = true; 4015 } 4016 if (gInBatchStack == 0) { 4017 this.saveState(); 4018 } else if (gInBatchStack < 0) { 4019 throw new Error( 4020 "The batch editing stack should never reach a negative number." 4021 ); 4022 } 4023 }, 4024 4025 /** 4026 * @see CustomizableUI.addListener 4027 * @param {CustomizableUIListener} aListener 4028 */ 4029 addListener(aListener) { 4030 gListeners.add(aListener); 4031 }, 4032 4033 /** 4034 * @see CustomizableUI.removeListener 4035 * @param {CustomizableUIListener} aListener 4036 */ 4037 removeListener(aListener) { 4038 if (aListener == this) { 4039 return; 4040 } 4041 4042 gListeners.delete(aListener); 4043 }, 4044 4045 /** 4046 * For any listeners registered via `addListener` or `removeListener`, this 4047 * calls the appropriate listener function for a particular CustomizableUI 4048 * event if it is defined on the listener, passing along the arguments. 4049 * 4050 * @param {string} aListenerName 4051 * The name of the listener that should be called. This is a string 4052 * identifier of something that can be listened for via a 4053 * CustomizableUIListener - for example, "onWidgetCreated". 4054 * @param {...any} aArgs 4055 * The arguments to pass to the CustomizableUIListener function. 4056 */ 4057 notifyListeners(aListenerName, ...aArgs) { 4058 if (gRestoring) { 4059 return; 4060 } 4061 4062 for (let listener of gListeners) { 4063 try { 4064 if (typeof listener[aListenerName] == "function") { 4065 listener[aListenerName].apply(listener, aArgs); 4066 } 4067 } catch (e) { 4068 lazy.log.error(e + " -- " + e.fileName + ":" + e.lineNumber); 4069 } 4070 } 4071 }, 4072 4073 /** 4074 * Constructs a CustomEvent with the aEventType type that is bubbling and 4075 * cancelable, and includes the details passed on aDetails. This event is 4076 * then dispatched on the gNavToolbox in aWindow. 4077 * 4078 * @see CustomizableUIInternal.dispatchToolboxEvent 4079 * @param {string} aEventType 4080 * The type of the CustomEvent to fire. 4081 * @param {any} aDetails 4082 * The details to assign to the event being fired. 4083 * @param {DOMWindow} aWindow 4084 * The browser window containing the gNavToolbox which will have the event 4085 * dispatched on it. 4086 */ 4087 _dispatchToolboxEventToWindow(aEventType, aDetails, aWindow) { 4088 let evt = new aWindow.CustomEvent(aEventType, { 4089 bubbles: true, 4090 cancelable: true, 4091 detail: aDetails, 4092 }); 4093 aWindow.gNavToolbox.dispatchEvent(evt); 4094 }, 4095 4096 /** 4097 * Constructs a CustomEvent with the aEventType type that is bubbling and 4098 * cancelable, and includes the details passed on aDetails. This event is 4099 * then dispatched on the gNavToolbox in aWindow if one is provided. If no 4100 * window is provided, this is dispatched on the gNavToolbox for all 4101 * registered windows. 4102 * 4103 * @param {string} aEventType 4104 * The type of the CustomEvent to fire. 4105 * @param {any} [aDetails={}] 4106 * The details to assign to the event being fired. 4107 * @param {DOMWindow} [aWindow=null] 4108 * The browser window containing the gNavToolbox which will have the event 4109 * dispatched on it, or null to dispatch to all gNavToolbox elements in 4110 * all registered windows. 4111 */ 4112 dispatchToolboxEvent(aEventType, aDetails = {}, aWindow = null) { 4113 if (aWindow) { 4114 this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow); 4115 return; 4116 } 4117 for (let [win] of gBuildWindows) { 4118 this._dispatchToolboxEventToWindow(aEventType, aDetails, win); 4119 } 4120 }, 4121 4122 /** 4123 * @see CustomizableUI.createWidget 4124 * @param {CustomizableUICreateWidgetProperties} aProperties 4125 * @returns {string} 4126 * The ID of the created widget. 4127 */ 4128 createWidget(aProperties) { 4129 let widget = this.normalizeWidget( 4130 aProperties, 4131 CustomizableUI.SOURCE_EXTERNAL 4132 ); 4133 // XXXunf This should probably throw. 4134 if (!widget) { 4135 lazy.log.error("unable to normalize widget"); 4136 return undefined; 4137 } 4138 4139 gPalette.set(widget.id, widget); 4140 4141 // Clear our caches: 4142 gGroupWrapperCache.delete(widget.id); 4143 for (let [win] of gBuildWindows) { 4144 let cache = gSingleWrapperCache.get(win); 4145 if (cache) { 4146 cache.delete(widget.id); 4147 } 4148 } 4149 4150 this.notifyListeners("onWidgetCreated", widget.id); 4151 4152 if (widget.defaultArea) { 4153 let addToDefaultPlacements = false; 4154 let area = gAreas.get(widget.defaultArea); 4155 if ( 4156 !CustomizableUI.isBuiltinToolbar(widget.defaultArea) && 4157 widget.defaultArea != CustomizableUI.AREA_FIXED_OVERFLOW_PANEL 4158 ) { 4159 addToDefaultPlacements = true; 4160 } 4161 4162 if (addToDefaultPlacements) { 4163 if (area.has("defaultPlacements")) { 4164 area.get("defaultPlacements").push(widget.id); 4165 } else { 4166 area.set("defaultPlacements", [widget.id]); 4167 } 4168 } 4169 } 4170 4171 // Look through previously saved state to see if we're restoring a widget. 4172 let seenAreas = new Set(); 4173 let widgetMightNeedAutoAdding = true; 4174 for (let [area] of gPlacements) { 4175 seenAreas.add(area); 4176 let areaIsRegistered = gAreas.has(area); 4177 let index = gPlacements.get(area).indexOf(widget.id); 4178 if (index != -1) { 4179 widgetMightNeedAutoAdding = false; 4180 if (areaIsRegistered) { 4181 widget.currentArea = area; 4182 widget.currentPosition = index; 4183 } 4184 break; 4185 } 4186 } 4187 4188 // Also look at saved state data directly in areas that haven't yet been 4189 // restored. Can't rely on this for restored areas, as they may have 4190 // changed. 4191 if (widgetMightNeedAutoAdding && gSavedState) { 4192 for (let area of Object.keys(gSavedState.placements)) { 4193 if (seenAreas.has(area)) { 4194 continue; 4195 } 4196 4197 let areaIsRegistered = gAreas.has(area); 4198 let index = gSavedState.placements[area].indexOf(widget.id); 4199 if (index != -1) { 4200 widgetMightNeedAutoAdding = false; 4201 if (areaIsRegistered) { 4202 widget.currentArea = area; 4203 widget.currentPosition = index; 4204 } 4205 break; 4206 } 4207 } 4208 } 4209 4210 // If we're restoring the widget to it's old placement, fire off the 4211 // onWidgetAdded event - our own handler will take care of adding it to 4212 // any build areas. 4213 this.beginBatchUpdate(); 4214 try { 4215 if (widget.currentArea) { 4216 this.notifyListeners( 4217 "onWidgetAdded", 4218 widget.id, 4219 widget.currentArea, 4220 widget.currentPosition 4221 ); 4222 } else if (widgetMightNeedAutoAdding) { 4223 let autoAdd = Services.prefs.getBoolPref( 4224 kPrefCustomizationAutoAdd, 4225 true 4226 ); 4227 4228 // If the widget doesn't have an existing placement, and it hasn't been 4229 // seen before, then add it to its default area so it can be used. 4230 // If the widget is not removable, we *have* to add it to its default 4231 // area here. 4232 let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id); 4233 if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) { 4234 if (widget.defaultArea) { 4235 if (this.isAreaLazy(widget.defaultArea)) { 4236 gFuturePlacements.get(widget.defaultArea).add(widget.id); 4237 } else { 4238 this.addWidgetToArea(widget.id, widget.defaultArea); 4239 } 4240 } 4241 } 4242 4243 // Extension widgets cannot enter the customization palette, so if 4244 // at this point, we haven't found an area for them, move them into 4245 // AREA_ADDONS. 4246 if ( 4247 !widget.currentArea && 4248 CustomizableUI.isWebExtensionWidget(widget.id) 4249 ) { 4250 this.addWidgetToArea(widget.id, CustomizableUI.AREA_ADDONS); 4251 } 4252 } 4253 } finally { 4254 // Ensure we always have this widget in gSeenWidgets, and save 4255 // state in case this needs to be done here. 4256 gSeenWidgets.add(widget.id); 4257 this.endBatchUpdate(true); 4258 } 4259 4260 this.notifyListeners( 4261 "onWidgetAfterCreation", 4262 widget.id, 4263 widget.currentArea 4264 ); 4265 return widget.id; 4266 }, 4267 4268 /** 4269 * Creates the widgets that are defined statically within the browser in 4270 * CustomizableWidgets. 4271 * 4272 * @param {CustomizableUICreateWidgetProperties} aData 4273 */ 4274 createBuiltinWidget(aData) { 4275 // This should only ever be called on startup, before any windows are 4276 // opened - so we know there's no build areas to handle. Also, builtin 4277 // widgets are expected to be (mostly) static, so shouldn't affect the 4278 // current placement settings. 4279 4280 // This allows a widget to be both built-in by default but also able to be 4281 // destroyed and removed from the area based on criteria that may not be 4282 // available when the widget is created -- for example, because some other 4283 // feature in the browser supersedes the widget. 4284 let conditionalDestroyPromise = aData.conditionalDestroyPromise || null; 4285 delete aData.conditionalDestroyPromise; 4286 4287 let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN); 4288 if (!widget) { 4289 lazy.log.error("Error creating builtin widget: " + aData.id); 4290 return; 4291 } 4292 4293 lazy.log.debug("Creating built-in widget with id: " + widget.id); 4294 gPalette.set(widget.id, widget); 4295 4296 if (conditionalDestroyPromise) { 4297 conditionalDestroyPromise.then( 4298 shouldDestroy => { 4299 if (shouldDestroy) { 4300 this.destroyWidget(widget.id); 4301 this.removeWidgetFromArea(widget.id); 4302 } 4303 }, 4304 err => { 4305 console.error(err); 4306 } 4307 ); 4308 } 4309 }, 4310 4311 /** 4312 * Returns true if the associated area will eventually lazily restore (but 4313 * hasn't yet). 4314 * 4315 * @param {string} aAreaId 4316 * @returns {boolean} 4317 */ 4318 isAreaLazy(aAreaId) { 4319 if (gPlacements.has(aAreaId) || !gAreas.has(aAreaId)) { 4320 return false; 4321 } 4322 return gAreas.get(aAreaId).get("type") == CustomizableUI.TYPE_TOOLBAR; 4323 }, 4324 4325 /** 4326 * Given a set of CustomizableUICreateWidgetProperties, attempts to 4327 * create a "normalized" version of that object with default values where 4328 * aData failed to define values, as well as properly wrapped event handlers. 4329 * 4330 * @param {CustomizableUICreateWidgetProperties} aData 4331 * @param {string} aSource 4332 * One of the CustomizableUI.SOURCE_* constants, for example 4333 * CustomizableUI.SOURCE_EXTERNAL. 4334 * @returns {object} 4335 * The normalized widget representation. Notably, the `implementation` 4336 * property of this widget will point to the original aData structure. 4337 */ 4338 normalizeWidget(aData, aSource) { 4339 let widget = { 4340 implementation: aData, 4341 source: aSource || CustomizableUI.SOURCE_EXTERNAL, 4342 instances: new Map(), 4343 currentArea: null, 4344 localized: true, 4345 removable: true, 4346 overflows: true, 4347 defaultArea: null, 4348 shortcutId: null, 4349 tabSpecific: false, 4350 locationSpecific: false, 4351 tooltiptext: null, 4352 l10nId: null, 4353 showInPrivateBrowsing: true, 4354 hideInNonPrivateBrowsing: false, 4355 _introducedInVersion: -1, 4356 _introducedByPref: null, 4357 keepBroadcastAttributesWhenCustomizing: false, 4358 disallowSubView: false, 4359 webExtension: false, 4360 }; 4361 4362 if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) { 4363 lazy.log.error("Given an illegal id in normalizeWidget: " + aData.id); 4364 return null; 4365 } 4366 4367 delete widget.implementation.currentArea; 4368 widget.implementation.__defineGetter__( 4369 "currentArea", 4370 () => widget.currentArea 4371 ); 4372 4373 const kReqStringProps = ["id"]; 4374 for (let prop of kReqStringProps) { 4375 if (typeof aData[prop] != "string") { 4376 lazy.log.error( 4377 "Missing required property '" + 4378 prop + 4379 "' in normalizeWidget: " + 4380 aData.id 4381 ); 4382 return null; 4383 } 4384 widget[prop] = aData[prop]; 4385 } 4386 4387 const kOptStringProps = ["l10nId", "label", "tooltiptext", "shortcutId"]; 4388 for (let prop of kOptStringProps) { 4389 if (typeof aData[prop] == "string") { 4390 widget[prop] = aData[prop]; 4391 } 4392 } 4393 4394 const kOptBoolProps = [ 4395 "removable", 4396 "showInPrivateBrowsing", 4397 "hideInNonPrivateBrowsing", 4398 "overflows", 4399 "tabSpecific", 4400 "locationSpecific", 4401 "localized", 4402 "keepBroadcastAttributesWhenCustomizing", 4403 "disallowSubView", 4404 "webExtension", 4405 ]; 4406 for (let prop of kOptBoolProps) { 4407 if (typeof aData[prop] == "boolean") { 4408 widget[prop] = aData[prop]; 4409 } 4410 } 4411 4412 // When we normalize builtin widgets, areas have not yet been registered: 4413 if ( 4414 aData.defaultArea && 4415 (aSource == CustomizableUI.SOURCE_BUILTIN || 4416 gAreas.has(aData.defaultArea)) 4417 ) { 4418 widget.defaultArea = aData.defaultArea; 4419 } else if (!widget.removable) { 4420 lazy.log.error( 4421 "Widget '" + 4422 widget.id + 4423 "' is not removable but does not specify " + 4424 "a valid defaultArea. That's not possible; it must specify a " + 4425 "valid defaultArea as well." 4426 ); 4427 return null; 4428 } 4429 4430 if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) { 4431 widget.type = aData.type; 4432 } else { 4433 widget.type = "button"; 4434 } 4435 4436 widget.disabled = aData.disabled === true; 4437 4438 if (aSource == CustomizableUI.SOURCE_BUILTIN) { 4439 widget._introducedInVersion = aData.introducedInVersion || 0; 4440 4441 if (aData._introducedByPref) { 4442 widget._introducedByPref = aData._introducedByPref; 4443 } 4444 } 4445 4446 this.wrapWidgetEventHandler("onBeforeCreated", widget); 4447 this.wrapWidgetEventHandler("onClick", widget); 4448 this.wrapWidgetEventHandler("onCreated", widget); 4449 this.wrapWidgetEventHandler("onDestroyed", widget); 4450 4451 if (typeof aData.onBeforeCommand == "function") { 4452 widget.onBeforeCommand = aData.onBeforeCommand; 4453 } 4454 4455 if (typeof aData.onCommand == "function") { 4456 widget.onCommand = aData.onCommand; 4457 } 4458 if ( 4459 widget.type == "view" || 4460 widget.type == "button-and-view" || 4461 aData.viewId 4462 ) { 4463 if (typeof aData.viewId != "string") { 4464 lazy.log.error( 4465 "Expected a string for widget " + 4466 widget.id + 4467 " viewId, but got " + 4468 aData.viewId 4469 ); 4470 return null; 4471 } 4472 widget.viewId = aData.viewId; 4473 4474 this.wrapWidgetEventHandler("onViewShowing", widget); 4475 this.wrapWidgetEventHandler("onViewHiding", widget); 4476 } 4477 if (widget.type == "custom") { 4478 this.wrapWidgetEventHandler("onBuild", widget); 4479 } 4480 4481 if (gPalette.has(widget.id)) { 4482 return null; 4483 } 4484 4485 return widget; 4486 }, 4487 4488 /** 4489 * Given some widget definition object, forwards calls to functions with the 4490 * name aEventName to the underlying implementation objects copy of that 4491 * function. 4492 * 4493 * @param {string} aEventName 4494 * The name of the function to redirect to the underlying implementations 4495 * version of that same named function. 4496 * @param {object} aWidget 4497 * A "normalized" widget definition as computed by `normalizeWidget()`. 4498 */ 4499 wrapWidgetEventHandler(aEventName, aWidget) { 4500 if (typeof aWidget.implementation[aEventName] != "function") { 4501 aWidget[aEventName] = null; 4502 return; 4503 } 4504 aWidget[aEventName] = function (...aArgs) { 4505 try { 4506 // Don't copy the function to the normalized widget object, instead 4507 // keep it on the original object provided to the API so that 4508 // additional methods can be implemented and used by the event 4509 // handlers. 4510 return aWidget.implementation[aEventName].apply( 4511 aWidget.implementation, 4512 aArgs 4513 ); 4514 } catch (e) { 4515 console.error(e); 4516 return undefined; 4517 } 4518 }; 4519 }, 4520 4521 /** 4522 * @see CustomizableUI.destroyWidget 4523 * @param {string} aWidgetId 4524 */ 4525 destroyWidget(aWidgetId) { 4526 let widget = gPalette.get(aWidgetId); 4527 if (!widget) { 4528 gGroupWrapperCache.delete(aWidgetId); 4529 for (let [window] of gBuildWindows) { 4530 let windowCache = gSingleWrapperCache.get(window); 4531 if (windowCache) { 4532 windowCache.delete(aWidgetId); 4533 } 4534 } 4535 return; 4536 } 4537 4538 // Remove it from the default placements of an area if it was added there: 4539 if (widget.defaultArea) { 4540 let area = gAreas.get(widget.defaultArea); 4541 if (area) { 4542 let defaultPlacements = area.get("defaultPlacements"); 4543 // We can assume this is present because if a widget has a defaultArea, 4544 // we automatically create a defaultPlacements array for that area. 4545 let widgetIndex = defaultPlacements.indexOf(aWidgetId); 4546 if (widgetIndex != -1) { 4547 defaultPlacements.splice(widgetIndex, 1); 4548 } 4549 } 4550 } 4551 4552 // This will not remove the widget from gPlacements - we want to keep the 4553 // setting so the widget gets put back in it's old position if/when it 4554 // returns. 4555 for (let [window] of gBuildWindows) { 4556 let windowCache = gSingleWrapperCache.get(window); 4557 if (windowCache) { 4558 windowCache.delete(aWidgetId); 4559 } 4560 let widgetNode = 4561 window.document.getElementById(aWidgetId) || 4562 window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; 4563 if (widgetNode) { 4564 let container = widgetNode.parentNode; 4565 this.notifyListeners( 4566 "onWidgetBeforeDOMChange", 4567 widgetNode, 4568 null, 4569 container, 4570 true 4571 ); 4572 widgetNode.remove(); 4573 this.notifyListeners( 4574 "onWidgetAfterDOMChange", 4575 widgetNode, 4576 null, 4577 container, 4578 true 4579 ); 4580 } 4581 if ( 4582 widget.type == "view" || 4583 widget.type == "button-and-view" || 4584 widget.viewId 4585 ) { 4586 let viewNode = window.document.getElementById(widget.viewId); 4587 if (viewNode) { 4588 for (let eventName of kSubviewEvents) { 4589 let handler = "on" + eventName; 4590 if (typeof widget[handler] == "function") { 4591 viewNode.removeEventListener(eventName, widget[handler]); 4592 } 4593 } 4594 viewNode._addedEventListeners = false; 4595 } 4596 } 4597 if (widgetNode && widget.onDestroyed) { 4598 widget.onDestroyed(window.document); 4599 } 4600 } 4601 4602 gPalette.delete(aWidgetId); 4603 gGroupWrapperCache.delete(aWidgetId); 4604 4605 this.notifyListeners("onWidgetDestroyed", aWidgetId); 4606 }, 4607 4608 /** 4609 * @see CustomizableUI.getCustomizeTargetForArea 4610 * @param {string} aArea 4611 * @param {DOMWindow} aWindow 4612 * @returns {Element} 4613 */ 4614 getCustomizeTargetForArea(aArea, aWindow) { 4615 let buildAreaNodes = gBuildAreas.get(aArea); 4616 if (!buildAreaNodes) { 4617 return null; 4618 } 4619 4620 for (let node of buildAreaNodes) { 4621 if (node.ownerGlobal == aWindow) { 4622 return this.getCustomizationTarget(node) || node; 4623 } 4624 } 4625 4626 return null; 4627 }, 4628 4629 /** 4630 * @see CustomizableUI.reset() 4631 */ 4632 reset() { 4633 gResetting = true; 4634 // CUI reset also implies resetting verticalTabs back to false. 4635 // We do this before the rest of the reset so widgets are reset to their non-vertical 4636 // positions. 4637 Services.prefs.setBoolPref("sidebar.verticalTabs", false); 4638 this._resetUIState(); 4639 4640 // Rebuild each registered area (across windows) to reflect the state that 4641 // was reset above. 4642 this._rebuildRegisteredAreas(); 4643 4644 for (let [widgetId, widget] of gPalette) { 4645 if (widget.source == CustomizableUI.SOURCE_EXTERNAL) { 4646 gSeenWidgets.add(widgetId); 4647 } 4648 } 4649 if (gSeenWidgets.size || gNewElementCount) { 4650 gDirty = true; 4651 this.saveState(); 4652 } 4653 4654 gResetting = false; 4655 }, 4656 4657 /** 4658 * Persists the current state to gUIStateBeforeReset (in order to temporarily 4659 * allow for undoing CustomizableUI resets) and then blows away the current 4660 * CustomizableUI state (including saved prefs) and sets them to their 4661 * defaults. This also rebuilds all of the registered areas to reflect the 4662 * defaults. 4663 */ 4664 _resetUIState() { 4665 try { 4666 gUIStateBeforeReset.drawInTitlebar = 4667 Services.prefs.getIntPref(kPrefDrawInTitlebar); 4668 gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref( 4669 kPrefCustomizationState 4670 ); 4671 gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity); 4672 gUIStateBeforeReset.autoTouchMode = 4673 Services.prefs.getBoolPref(kPrefAutoTouchMode); 4674 gUIStateBeforeReset.currentTheme = gSelectedTheme; 4675 gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref( 4676 kPrefAutoHideDownloadsButton 4677 ); 4678 gUIStateBeforeReset.newElementCount = gNewElementCount; 4679 gUIStateBeforeReset.sidebarPositionStart = Services.prefs.getBoolPref( 4680 kPrefSidebarPositionStartEnabled 4681 ); 4682 } catch (e) {} 4683 4684 Services.prefs.clearUserPref(kPrefCustomizationState); 4685 Services.prefs.clearUserPref(kPrefDrawInTitlebar); 4686 Services.prefs.clearUserPref(kPrefUIDensity); 4687 Services.prefs.clearUserPref(kPrefAutoTouchMode); 4688 Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton); 4689 Services.prefs.clearUserPref(kPrefSidebarPositionStartEnabled); 4690 gDefaultTheme.enable(); 4691 gNewElementCount = 0; 4692 lazy.log.debug("State reset"); 4693 4694 // Later in the function, we're going to add any area-less extension 4695 // buttons to the AREA_ADDONS area. We'll remember the old placements 4696 // for that area so that we don't need to re-add widgets that are already 4697 // in there in the DOM. 4698 let oldAddonPlacements = gPlacements[CustomizableUI.AREA_ADDONS] || []; 4699 4700 // Reset placements to make restoring default placements possible. 4701 gPlacements = new Map(); 4702 gDirtyAreaCache = new Set(); 4703 gSeenWidgets = new Set(); 4704 // Clear the saved state to ensure that defaults will be used. 4705 gSavedState = null; 4706 // Restore the state for each area to its defaults 4707 for (let [areaId] of gAreas) { 4708 // If the Unified Extensions UI is enabled, we'll be adding any 4709 // extension buttons that aren't already in AREA_ADDONS there, 4710 // so we can skip restoring the state for it. 4711 if (areaId != CustomizableUI.AREA_ADDONS) { 4712 this.restoreStateForArea(areaId); 4713 } 4714 } 4715 4716 // restoreStateForArea will have normally set an array for the placements 4717 // for each area, but since we skip AREA_ADDONS intentionally, that array 4718 // doesn't get set, so we do that manually here. 4719 gPlacements.set(CustomizableUI.AREA_ADDONS, []); 4720 4721 for (let [widgetId] of gPalette) { 4722 if ( 4723 CustomizableUI.isWebExtensionWidget(widgetId) && 4724 !oldAddonPlacements.includes(widgetId) 4725 ) { 4726 // When resetting, NoScript goes to the toolbar instead. This matches 4727 // its initial placement anyway. And since the button may be hidden by 4728 // default by extensions.hideNoScript, we want to make sure that if it 4729 // becomes unhidden it is shown rather than in the unified extensions 4730 // panel. See tor-browser#41581. 4731 this.addWidgetToArea( 4732 widgetId, 4733 widgetId === NoScriptId 4734 ? CustomizableUI.AREA_NAVBAR 4735 : CustomizableUI.AREA_ADDONS 4736 ); 4737 } 4738 } 4739 }, 4740 4741 /** 4742 * For all registered areas, builds those areas to reflect the current 4743 * placement state of all widgets. 4744 */ 4745 _rebuildRegisteredAreas() { 4746 for (let [areaId, areaNodes] of gBuildAreas) { 4747 let placements = gPlacements.get(areaId); 4748 let isFirstChangedToolbar = true; 4749 for (let areaNode of areaNodes) { 4750 this.buildArea(areaId, placements, areaNode); 4751 4752 let area = gAreas.get(areaId); 4753 if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { 4754 let defaultCollapsed = area.get("defaultCollapsed"); 4755 let win = areaNode.ownerGlobal; 4756 if (defaultCollapsed !== null) { 4757 win.setToolbarVisibility( 4758 areaNode, 4759 typeof defaultCollapsed == "string" 4760 ? defaultCollapsed 4761 : !defaultCollapsed, 4762 isFirstChangedToolbar 4763 ); 4764 } 4765 } 4766 isFirstChangedToolbar = false; 4767 } 4768 } 4769 }, 4770 4771 /** 4772 * Undoes a previous reset, restoring the state of the UI to the state prior 4773 * to the reset. 4774 */ 4775 undoReset() { 4776 if ( 4777 gUIStateBeforeReset.uiCustomizationState == null || 4778 gUIStateBeforeReset.drawInTitlebar == null 4779 ) { 4780 return; 4781 } 4782 gUndoResetting = true; 4783 4784 const { 4785 uiCustomizationState, 4786 drawInTitlebar, 4787 currentTheme, 4788 uiDensity, 4789 autoTouchMode, 4790 autoHideDownloadsButton, 4791 sidebarPositionStart, 4792 } = gUIStateBeforeReset; 4793 gNewElementCount = gUIStateBeforeReset.newElementCount; 4794 4795 // Need to clear the previous state before setting the prefs 4796 // because pref observers may check if there is a previous UI state. 4797 this._clearPreviousUIState(); 4798 4799 Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); 4800 Services.prefs.setIntPref(kPrefDrawInTitlebar, drawInTitlebar); 4801 Services.prefs.setIntPref(kPrefUIDensity, uiDensity); 4802 Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode); 4803 Services.prefs.setBoolPref( 4804 kPrefAutoHideDownloadsButton, 4805 autoHideDownloadsButton 4806 ); 4807 Services.prefs.setBoolPref( 4808 kPrefSidebarPositionStartEnabled, 4809 sidebarPositionStart 4810 ); 4811 currentTheme.enable(); 4812 this.loadSavedState(); 4813 // If the user just customizes toolbar/titlebar visibility, gSavedState will be null 4814 // and we don't need to do anything else here: 4815 if (gSavedState) { 4816 for (let areaId of Object.keys(gSavedState.placements)) { 4817 let placements = gSavedState.placements[areaId]; 4818 gPlacements.set(areaId, placements); 4819 } 4820 this._rebuildRegisteredAreas(); 4821 } 4822 4823 gUndoResetting = false; 4824 }, 4825 4826 /** 4827 * Clears the persisted state that was snapshotted just before the most 4828 * recent reset of the state. 4829 */ 4830 _clearPreviousUIState() { 4831 Object.getOwnPropertyNames(gUIStateBeforeReset).forEach(prop => { 4832 gUIStateBeforeReset[prop] = null; 4833 }); 4834 }, 4835 4836 /** 4837 * @param {string|Node} aWidget 4838 * Widget ID or a widget node (preferred for performance). 4839 * @returns {boolean} 4840 * True if the widget is removable. 4841 */ 4842 isWidgetRemovable(aWidget) { 4843 let widgetId; 4844 let widgetNode; 4845 if (typeof aWidget == "string") { 4846 widgetId = aWidget; 4847 } else { 4848 // Skipped items could just not have ids. 4849 if (!aWidget.id && aWidget.getAttribute("skipintoolbarset") == "true") { 4850 return false; 4851 } 4852 if ( 4853 !aWidget.id && 4854 !["toolbarspring", "toolbarspacer", "toolbarseparator"].includes( 4855 aWidget.nodeName 4856 ) 4857 ) { 4858 throw new Error( 4859 "No nodes without ids that aren't special widgets should ever come into contact with CUI" 4860 ); 4861 } 4862 // Use "spring" / "spacer" / "separator" for special widgets without ids 4863 widgetId = 4864 aWidget.id || aWidget.nodeName.substring(7 /* "toolbar".length */); 4865 widgetNode = aWidget; 4866 } 4867 let provider = this.getWidgetProvider(widgetId); 4868 4869 if (provider == CustomizableUI.PROVIDER_API) { 4870 return gPalette.get(widgetId).removable; 4871 } 4872 4873 if (provider == CustomizableUI.PROVIDER_XUL) { 4874 if (gBuildWindows.size == 0) { 4875 // We don't have any build windows to look at, so just assume for now 4876 // that its removable. 4877 return true; 4878 } 4879 4880 if (!widgetNode) { 4881 // Pick any of the build windows to look at. 4882 let [window] = [...gBuildWindows][0]; 4883 [, widgetNode] = this.getWidgetNode(widgetId, window); 4884 } 4885 // If we don't have a node, we assume it's removable. This can happen because 4886 // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen 4887 // for API-provided widgets which have been destroyed. 4888 if (!widgetNode) { 4889 return true; 4890 } 4891 return widgetNode.getAttribute("removable") == "true"; 4892 } 4893 4894 // Otherwise this is either a special widget, which is always removable, or 4895 // an API widget which has already been removed from gPalette. Returning true 4896 // here allows us to then remove its ID from any placements where it might 4897 // still occur. 4898 return true; 4899 }, 4900 4901 /** 4902 * @see CustomizableUI.canWidgetMoveToArea 4903 * @param {string} aWidgetId 4904 * @param {string} aArea 4905 * @returns {boolean} 4906 */ 4907 canWidgetMoveToArea(aWidgetId, aArea) { 4908 // Special widgets can't move to the menu panel. 4909 if ( 4910 this.isSpecialWidget(aWidgetId) && 4911 gAreas.has(aArea) && 4912 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL 4913 ) { 4914 return false; 4915 } 4916 4917 if ( 4918 aArea == CustomizableUI.AREA_ADDONS && 4919 !CustomizableUI.isWebExtensionWidget(aWidgetId) 4920 ) { 4921 return false; 4922 } 4923 4924 if (CustomizableUI.isWebExtensionWidget(aWidgetId)) { 4925 // Extension widgets cannot move to the customization palette. 4926 if (aArea == CustomizableUI.AREA_NO_AREA) { 4927 return false; 4928 } 4929 4930 // Extension widgets cannot move to panels, with the exception of the 4931 // AREA_ADDONS area. 4932 if ( 4933 gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL && 4934 aArea != CustomizableUI.AREA_ADDONS 4935 ) { 4936 return false; 4937 } 4938 } 4939 4940 let placement = this.getPlacementOfWidget(aWidgetId); 4941 // Items in the palette can move, and items can move within their area: 4942 if (!placement || placement.area == aArea) { 4943 return true; 4944 } 4945 // For everything else, just return whether the widget is removable. 4946 return this.isWidgetRemovable(aWidgetId); 4947 }, 4948 4949 /** 4950 * @see CustomizableUI.ensureWidgetPlacedInWindow 4951 * @param {string} aWidgetId 4952 * @param {DOMWindow} aWindow 4953 * @returns {boolean} 4954 */ 4955 ensureWidgetPlacedInWindow(aWidgetId, aWindow) { 4956 let placement = this.getPlacementOfWidget(aWidgetId); 4957 if (!placement) { 4958 return false; 4959 } 4960 let areaNodes = gBuildAreas.get(placement.area); 4961 if (!areaNodes) { 4962 return false; 4963 } 4964 let container = [...areaNodes].filter(n => n.ownerGlobal == aWindow); 4965 if (!container.length) { 4966 return false; 4967 } 4968 let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0]; 4969 if (existingNode) { 4970 return true; 4971 } 4972 4973 this.insertNodeInWindow(aWidgetId, container[0], true); 4974 return true; 4975 }, 4976 4977 /** 4978 * Returns a list of all the widget IDs actively in this container, including 4979 * any that are overflown for overflowable containers. Notably, this does NOT 4980 * include IDs of widgets that have been previously placed within this 4981 * container but are not currently registered (for example, for uninstalled 4982 * extensions). 4983 * 4984 * @param {Element} container 4985 * @returns {string[]} 4986 * The list of widget IDs that currently exist within container. 4987 */ 4988 _getCurrentWidgetsInContainer(container) { 4989 let currentWidgets = new Set(); 4990 function addUnskippedChildren(parent) { 4991 for (let node of parent.children) { 4992 let realNode = 4993 node.localName == "toolbarpaletteitem" 4994 ? node.firstElementChild 4995 : node; 4996 if (realNode.getAttribute("skipintoolbarset") != "true") { 4997 currentWidgets.add(realNode.id); 4998 } 4999 } 5000 } 5001 addUnskippedChildren(this.getCustomizationTarget(container)); 5002 if (container.getAttribute("overflowing") == "true") { 5003 let overflowTarget = container.getAttribute("default-overflowtarget"); 5004 addUnskippedChildren( 5005 container.ownerDocument.getElementById(overflowTarget) 5006 ); 5007 let webExtOverflowTarget = container.getAttribute( 5008 "addon-webext-overflowtarget" 5009 ); 5010 addUnskippedChildren( 5011 container.ownerDocument.getElementById(webExtOverflowTarget) 5012 ); 5013 } 5014 // Then get the sorted list of placements, and filter based on the nodes 5015 // that are present. This avoids including items that don't exist (e.g. ids 5016 // of add-on items that the user has uninstalled). 5017 let orderedPlacements = CustomizableUI.getWidgetIdsInArea(container.id); 5018 return orderedPlacements.filter(w => { 5019 return ( 5020 currentWidgets.has(w) || 5021 this.getWidgetProvider(w) == CustomizableUI.PROVIDER_API 5022 ); 5023 }); 5024 }, 5025 5026 /** 5027 * @type {boolean} 5028 * True if the CustomizableUI state of the browser is in the stock state 5029 * that is shipped by default. 5030 */ 5031 get inDefaultState() { 5032 if (CustomizableUI.verticalTabsEnabled) { 5033 return false; 5034 } 5035 for (let [areaId, props] of gAreas) { 5036 let defaultPlacements = props 5037 .get("defaultPlacements") 5038 .filter(item => this.widgetExists(item)); 5039 let currentPlacements = gPlacements.get(areaId); 5040 // We're excluding all of the placement IDs for items that do not exist, 5041 // and items that have removable="false", 5042 // because we don't want to consider them when determining if we're 5043 // in the default state. This way, if an add-on introduces a widget 5044 // and is then uninstalled, the leftover placement doesn't cause us to 5045 // automatically assume that the buttons are not in the default state. 5046 let buildAreaNodes = gBuildAreas.get(areaId); 5047 if (buildAreaNodes && buildAreaNodes.size) { 5048 let container = [...buildAreaNodes][0]; 5049 let removableOrDefault = itemNodeOrItem => { 5050 let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem; 5051 let isRemovable = this.isWidgetRemovable(itemNodeOrItem); 5052 let isInDefault = defaultPlacements.includes(item); 5053 return isRemovable || isInDefault; 5054 }; 5055 // Toolbars need to deal with overflown widgets (if any) - so 5056 // specialcase them: 5057 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 5058 currentPlacements = 5059 this._getCurrentWidgetsInContainer(container).filter( 5060 removableOrDefault 5061 ); 5062 } else { 5063 currentPlacements = currentPlacements.filter(item => { 5064 let itemNode = container.getElementsByAttribute("id", item)[0]; 5065 return itemNode && removableOrDefault(itemNode || item); 5066 }); 5067 } 5068 5069 if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { 5070 let collapsed = null; 5071 let defaultCollapsed = props.get("defaultCollapsed"); 5072 let nondefaultState = false; 5073 if (areaId == CustomizableUI.AREA_BOOKMARKS) { 5074 collapsed = Services.prefs.getCharPref( 5075 "browser.toolbars.bookmarks.visibility" 5076 ); 5077 nondefaultState = Services.prefs.prefHasUserValue( 5078 "browser.toolbars.bookmarks.visibility" 5079 ); 5080 } else { 5081 let attribute = 5082 container.getAttribute("type") == "menubar" 5083 ? "autohide" 5084 : "collapsed"; 5085 collapsed = container.hasAttribute(attribute); 5086 nondefaultState = collapsed != defaultCollapsed; 5087 } 5088 if (defaultCollapsed !== null && nondefaultState) { 5089 lazy.log.debug( 5090 "Found " + 5091 areaId + 5092 " had non-default toolbar visibility" + 5093 "(expected " + 5094 defaultCollapsed + 5095 ", was " + 5096 collapsed + 5097 ")" 5098 ); 5099 return false; 5100 } 5101 } 5102 } 5103 lazy.log.debug( 5104 "Checking default state for " + 5105 areaId + 5106 ":\n" + 5107 currentPlacements.join(",") + 5108 "\nvs.\n" + 5109 defaultPlacements.join(",") 5110 ); 5111 5112 if (currentPlacements.length != defaultPlacements.length) { 5113 return false; 5114 } 5115 5116 for (let i = 0; i < currentPlacements.length; ++i) { 5117 if ( 5118 currentPlacements[i] != defaultPlacements[i] && 5119 !this.matchingSpecials(currentPlacements[i], defaultPlacements[i]) 5120 ) { 5121 lazy.log.debug( 5122 "Found " + 5123 currentPlacements[i] + 5124 " in " + 5125 areaId + 5126 " where " + 5127 defaultPlacements[i] + 5128 " was expected!" 5129 ); 5130 return false; 5131 } 5132 } 5133 } 5134 5135 if (Services.prefs.prefHasUserValue(kPrefUIDensity)) { 5136 lazy.log.debug(kPrefUIDensity + " pref is non-default"); 5137 return false; 5138 } 5139 5140 if (Services.prefs.prefHasUserValue(kPrefAutoTouchMode)) { 5141 lazy.log.debug(kPrefAutoTouchMode + " pref is non-default"); 5142 return false; 5143 } 5144 5145 if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) { 5146 lazy.log.debug(kPrefDrawInTitlebar + " pref is non-default"); 5147 return false; 5148 } 5149 5150 // This should just be `gDefaultTheme.isActive`, but bugs... 5151 if (gDefaultTheme && gDefaultTheme.id != gSelectedTheme.id) { 5152 lazy.log.debug(gSelectedTheme.id + " theme is non-default"); 5153 return false; 5154 } 5155 5156 if (Services.prefs.prefHasUserValue(kPrefSidebarPositionStartEnabled)) { 5157 lazy.log.debug(kPrefSidebarPositionStartEnabled + " pref is non-default"); 5158 return false; 5159 } 5160 5161 return true; 5162 }, 5163 5164 /** 5165 * @see CustomizableUI.getCollapsedToolbarIds 5166 * @param {Window} window 5167 * @returns {Set<string>} 5168 */ 5169 getCollapsedToolbarIds(window) { 5170 let collapsedToolbars = new Set(); 5171 for (let toolbarId of CustomizableUIInternal.builtinToolbars) { 5172 let toolbar = window.document.getElementById(toolbarId); 5173 5174 // Menubar toolbars are special in that they're hidden with the autohide 5175 // attribute. 5176 let hidingAttribute = 5177 toolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; 5178 5179 if (toolbar.hasAttribute(hidingAttribute)) { 5180 collapsedToolbars.add(toolbarId); 5181 } 5182 } 5183 5184 return collapsedToolbars; 5185 }, 5186 5187 /** 5188 * @see CustomizableUI.setToolbarVisibility 5189 * @param {string} aToolbarId 5190 * @param {boolean} aIsVisible 5191 */ 5192 setToolbarVisibility(aToolbarId, aIsVisible) { 5193 // We only persist the attribute the first time. 5194 let isFirstChangedToolbar = true; 5195 for (let window of CustomizableUI.windows) { 5196 let toolbar = window.document.getElementById(aToolbarId); 5197 if (toolbar) { 5198 window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar); 5199 isFirstChangedToolbar = false; 5200 } 5201 } 5202 }, 5203 5204 /** 5205 * @see CustomizableUI.widgetIsLikelyVisible 5206 * @param {string} aWidgetId 5207 * @param {Window} window 5208 * @returns {boolean} 5209 */ 5210 widgetIsLikelyVisible(aWidgetId, window) { 5211 let placement = this.getPlacementOfWidget(aWidgetId); 5212 5213 if (!placement) { 5214 return false; 5215 } 5216 5217 switch (placement.area) { 5218 case CustomizableUI.AREA_NAVBAR: 5219 return true; 5220 case CustomizableUI.AREA_MENUBAR: 5221 return !this.getCollapsedToolbarIds(window).has( 5222 CustomizableUI.AREA_MENUBAR 5223 ); 5224 case CustomizableUI.AREA_TABSTRIP: 5225 return !CustomizableUI.verticalTabsEnabled; 5226 case CustomizableUI.AREA_BOOKMARKS: 5227 return ( 5228 Services.prefs.getCharPref( 5229 "browser.toolbars.bookmarks.visibility" 5230 ) === "always" 5231 ); 5232 default: 5233 return false; 5234 } 5235 }, 5236 5237 /** 5238 * nsIObserver implementation that observes for toolbar visibility changes 5239 * or preference changes. 5240 * 5241 * @param {nsISupports} aSubject 5242 * @param {string} aTopic 5243 * @param {string} aData 5244 */ 5245 observe(aSubject, aTopic, aData) { 5246 if (aTopic == "browser-set-toolbar-visibility") { 5247 let [toolbar, visibility] = JSON.parse(aData); 5248 CustomizableUI.setToolbarVisibility(toolbar, visibility == "true"); 5249 } 5250 5251 if (aTopic === "nsPref:changed") { 5252 this.reconcileSidebarPrefs(aData); 5253 } 5254 }, 5255 5256 /** 5257 * Initializes CustomizableUI for the current tab orientation. 5258 * 5259 * @param {boolean} toVertical 5260 * True if the tab orientation is vertical, false if horizontal. 5261 */ 5262 initializeForTabsOrientation(toVertical) { 5263 lazy.log.debug( 5264 `initializeForTabsOrientation, toVertical: ${toVertical}, gCurrentVerticalTabs: ${gCurrentVerticalTabs}` 5265 ); 5266 if (!toVertical) { 5267 const savedPlacements = this.getSavedHorizontalSnapshotState(); 5268 lazy.log.debug( 5269 "initializeForTabsOrientation, savedPlacements", 5270 savedPlacements 5271 ); 5272 if (savedPlacements.length) { 5273 // We're startup up with horizontal tabs, but there are saved placements for the 5274 // horizontal tab strip, so its possible the verticalTabs pref was updated outside 5275 // of normal use. Make sure to restore those tabstrip widget placements 5276 this.restoreSavedHorizontalTabStripState(savedPlacements, true); 5277 } else { 5278 // This is the default state and normal initialization will do everything necessary 5279 } 5280 gCurrentVerticalTabs = false; 5281 return; 5282 } 5283 5284 // If the UI was already customized and saved, the earlier call to loadSavedState will 5285 // have populated gSavedState from the pref. If not, we need to move the tabs into the 5286 // vertical tabs area in the gSavedState. Then, the normal build-areas lifecycle 5287 // can populate the needed toolbar placements and elements. 5288 lazy.log.debug( 5289 "initializeForTabsOrientation, toVertical=true, gSavedState", 5290 gSavedState 5291 ); 5292 5293 // If there are saved placement customizations, we need to manually move widgets 5294 // around before we restore this state 5295 let savedPlacements = gSavedState?.placements || {}; 5296 if (!savedPlacements[CustomizableUI.AREA_VERTICAL_TABSTRIP]?.length) { 5297 savedPlacements[CustomizableUI.AREA_VERTICAL_TABSTRIP] = 5298 gAreas 5299 .get(CustomizableUI.AREA_VERTICAL_TABSTRIP) 5300 .get("verticalTabsDefaultPlacements") || []; 5301 lazy.log.debug( 5302 "initializeForTabsOrientation, using defaults for AREA_VERTICAL_TABSTRIP", 5303 savedPlacements[CustomizableUI.AREA_VERTICAL_TABSTRIP] 5304 ); 5305 } 5306 let tabstripPlacements = 5307 savedPlacements[CustomizableUI.AREA_TABSTRIP] || []; 5308 // also pick up any widgets already in gFuturePlacements so we can wipe that 5309 if (gFuturePlacements.has(CustomizableUI.AREA_TABSTRIP)) { 5310 for (let id of gFuturePlacements.get(CustomizableUI.AREA_TABSTRIP)) { 5311 if (!tabstripPlacements.includes(id)) { 5312 tabstripPlacements.push(id); 5313 } 5314 } 5315 gFuturePlacements.delete(CustomizableUI.AREA_TABSTRIP); 5316 } 5317 // Take a copy we can save and restore to, ensuring there's a sane default 5318 let savedTabstripPlacements = tabstripPlacements.length 5319 ? [...tabstripPlacements] 5320 : gAreas.get(CustomizableUI.AREA_TABSTRIP).get("defaultPlacements"); 5321 5322 // now we can remove the saved placements so they don't get picked back up again later in startup 5323 delete savedPlacements[CustomizableUI.AREA_TABSTRIP]; 5324 5325 let widgetsMoved = []; 5326 for (let widgetId of tabstripPlacements) { 5327 if (widgetId == "tabbrowser-tabs") { 5328 lazy.log.debug( 5329 `Moving saved tabbrowser-tabs to AREA_VERTICAL_TABSTRIP` 5330 ); 5331 this.addWidgetToArea( 5332 widgetId, 5333 CustomizableUI.AREA_VERTICAL_TABSTRIP, 5334 null, 5335 true 5336 ); 5337 continue; 5338 } 5339 // if this is a extension, those are handled in a toolbarvisibilitychange handler in browser-addons.js 5340 if (CustomizableUI.isWebExtensionWidget(widgetId)) { 5341 lazy.log.debug(`Skipping a webextension saved placement ${widgetId}`); 5342 continue; 5343 } 5344 // Everything else gets moved to the nav-bar area while tabs are vertical 5345 lazy.log.debug(`Moving saved placement ${widgetId} to nav-bar`); 5346 this.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR, null, true); 5347 widgetsMoved.push(widgetId); 5348 } 5349 lazy.log.debug( 5350 "initializeForTabsOrientation, widgets moved:", 5351 widgetsMoved 5352 ); 5353 if (widgetsMoved.length) { 5354 // We've updated the areas, so we don't need to do this again post-initialization 5355 gCurrentVerticalTabs = true; 5356 } 5357 5358 // Remove new tab from AREA_NAVBAR when vertical tabs enabled. 5359 this.removeWidgetFromArea("new-tab-button"); 5360 5361 // If we've ended up with a non-default CUI state and vertical tabs enabled, ensure 5362 // there's a sane snapshot to revert to 5363 if (!lazy.horizontalPlacementsPref) { 5364 lazy.log.debug( 5365 `verticalTabsEnabled but ${kPrefCustomizationHorizontalTabstrip} is empty` 5366 ); 5367 CustomizableUIInternal.saveHorizontalTabStripState( 5368 savedTabstripPlacements 5369 ); 5370 } 5371 }, 5372 5373 /** 5374 * Currently, the new sidebar and vertical tabs have a tight relationship 5375 * with one another (specifically, the new sidebar is a dependency for 5376 * vertical tabs). They are, however, controlled by two separate preferences. 5377 * This function does the work of changing the vertical tabs state if the 5378 * sidebar pref changes, and vice-versa. 5379 * 5380 * @param {string} prefChanged 5381 * The key for the preference that changed. 5382 */ 5383 reconcileSidebarPrefs(prefChanged) { 5384 let sidebarRevampEnabled = Services.prefs.getBoolPref( 5385 kPrefSidebarRevampEnabled, 5386 false 5387 ); 5388 let verticalTabsEnabled = Services.prefs.getBoolPref( 5389 kPrefSidebarVerticalTabsEnabled, 5390 false 5391 ); 5392 let positionStartEnabled = Services.prefs.getBoolPref( 5393 kPrefSidebarPositionStartEnabled, 5394 true 5395 ); 5396 lazy.log.debug( 5397 `reconcileSidebarPrefs, kPrefSidebarRevampEnabled: {sidebarRevampEnabled}, kPrefSidebarVerticalTabsEnabled: ${verticalTabsEnabled}` 5398 ); 5399 switch (prefChanged) { 5400 case kPrefSidebarVerticalTabsEnabled: { 5401 // We need to also enable sidebar.revamp if vertical tabs gets enabled 5402 if (verticalTabsEnabled && !sidebarRevampEnabled) { 5403 Services.prefs.setBoolPref(kPrefSidebarRevampEnabled, true); 5404 } 5405 break; 5406 } 5407 case kPrefSidebarRevampEnabled: { 5408 // If we are changing the pref after startup, update the nav bar defaultPlacements to include/exclude sidebar-button 5409 let props = gAreas.get(CustomizableUI.AREA_NAVBAR); 5410 let defaults = props.get("defaultPlacements"); 5411 let sidebarButtonIndex = defaults.indexOf("sidebar-button"); 5412 if (sidebarRevampEnabled && sidebarButtonIndex < 0) { 5413 defaults.unshift("sidebar-button"); 5414 } else if (!sidebarRevampEnabled && sidebarButtonIndex > -1) { 5415 defaults.splice(sidebarButtonIndex, 1); 5416 } 5417 props.set("defaultPlacements", defaults); 5418 gAreas.set(CustomizableUI.AREA_NAVBAR, props); 5419 // We need to also disable vertical tabs if sidebar.revamp is no longer enabled 5420 if (!sidebarRevampEnabled && verticalTabsEnabled) { 5421 lazy.log.debug( 5422 `{kPrefSidebarRevampEnabled} disabled, so also disabling ${kPrefSidebarVerticalTabsEnabled}` 5423 ); 5424 Services.prefs.setBoolPref(kPrefSidebarVerticalTabsEnabled, false); 5425 } 5426 break; 5427 } 5428 case kPrefSidebarPositionStartEnabled: { 5429 // If the sidebar moves to the left or right, move the toolbar button along with it. 5430 const navbarPlacements = gPlacements.get(CustomizableUI.AREA_NAVBAR); 5431 const index = navbarPlacements.indexOf("sidebar-button"); 5432 if (!positionStartEnabled && index === 0) { 5433 this.moveWidgetWithinArea("sidebar-button", navbarPlacements.length); 5434 } 5435 if (positionStartEnabled && index === navbarPlacements.length - 1) { 5436 this.moveWidgetWithinArea("sidebar-button", 0); 5437 } 5438 } 5439 } 5440 }, 5441 5442 /** 5443 * @type {boolean} 5444 * True if the horizontal and vertical tabstrips have been registered. 5445 */ 5446 get tabstripAreasReady() { 5447 return ( 5448 gBuildAreas.get(CustomizableUI.AREA_TABSTRIP)?.size && 5449 gBuildAreas.get(CustomizableUI.AREA_VERTICAL_TABSTRIP)?.size 5450 ); 5451 }, 5452 5453 /** 5454 * Updates the vertical or horizontal state of the tabstrip to best match 5455 * the current preference value. 5456 */ 5457 updateTabStripOrientation() { 5458 if (!this.tabstripAreasReady) { 5459 lazy.log.debug("tabstrip build areas not yet ready"); 5460 return; 5461 } 5462 let toVertical = CustomizableUI.verticalTabsEnabled; 5463 if (toVertical === gCurrentVerticalTabs) { 5464 lazy.log.debug("early return as the value hasn't changed"); 5465 return; 5466 } 5467 lazy.log.debug( 5468 `verticalTabs changed, from ${gCurrentVerticalTabs}, to ${toVertical}` 5469 ); 5470 5471 if (toVertical && gCurrentVerticalTabs !== null) { 5472 // Stash current placements as a state we can restore to when going back to horizontal tabs 5473 lazy.log.debug( 5474 "Switching to vertical tabs post-initialization, so capturing tabstrip placements snapshot" 5475 ); 5476 CustomizableUIInternal.saveHorizontalTabStripState(); 5477 } 5478 gCurrentVerticalTabs = toVertical; 5479 5480 function changeWidgetRemovability(widgetId, removable) { 5481 let widget = CustomizableUI.getWidget(widgetId); 5482 for (let { node } of widget.instances) { 5483 if (node) { 5484 node.setAttribute("removable", removable.toString()); 5485 } 5486 } 5487 } 5488 5489 // Normally these aren't removable, but for this operation only we need to move them 5490 changeWidgetRemovability("tabbrowser-tabs", true); 5491 5492 if (toVertical) { 5493 lazy.log.debug( 5494 `Switching to verticalTabs=true in updateTabStripOrientation` 5495 ); 5496 gDirty = true; 5497 5498 if ( 5499 !Services.prefs.getCharPref(kPrefCustomizationHorizontalTabsBackup, "") 5500 ) { 5501 // Before we switch for the first time, take a back up just in case we need an escape hatch 5502 Services.prefs.setCharPref( 5503 kPrefCustomizationHorizontalTabsBackup, 5504 Services.prefs.getCharPref(kPrefCustomizationState, "") 5505 ); 5506 } 5507 5508 CustomizableUI.beginBatchUpdate(); 5509 let customVerticalNavbarPlacements = this.getSavedVerticalSnapshotState(); 5510 let tabstripPlacements = this.getSavedHorizontalSnapshotState(); 5511 const isSidebarLast = 5512 gPlacements.get(CustomizableUI.AREA_NAVBAR).at(-1) === "sidebar-button"; 5513 // Remove non-default widgets to the nav-bar 5514 for (let id of CustomizableUI.getWidgetIdsInArea("TabsToolbar")) { 5515 if (id == "tabbrowser-tabs") { 5516 CustomizableUI.addWidgetToArea( 5517 id, 5518 CustomizableUI.AREA_VERTICAL_TABSTRIP 5519 ); 5520 continue; 5521 } 5522 // We add the tab strip placements later in the case they have a custom position 5523 if ( 5524 tabstripPlacements.includes(id) && 5525 customVerticalNavbarPlacements.includes(id) 5526 ) { 5527 continue; 5528 } 5529 if (!CustomizableUI.isWidgetRemovable(id)) { 5530 continue; 5531 } 5532 // if this is a extension, those are handled in a toolbarvisibilitychange handler in browser-addons.js 5533 if (CustomizableUI.isWebExtensionWidget(id)) { 5534 continue; 5535 } 5536 // Everything else gets moved to the nav-bar area while tabs are vertical 5537 CustomizableUI.addWidgetToArea(id, CustomizableUI.AREA_NAVBAR); 5538 } 5539 // Remove new tab from nav-bar when vertical tabs enabled 5540 this.removeWidgetFromArea("new-tab-button"); 5541 customVerticalNavbarPlacements.forEach((id, index) => { 5542 if (tabstripPlacements.includes(id)) { 5543 CustomizableUI.addWidgetToArea(id, CustomizableUI.AREA_NAVBAR, index); 5544 } 5545 }); 5546 // If sidebar was previously the last widget in navbar, carry it over to 5547 // the end of the newly constructed navbar. 5548 if (isSidebarLast) { 5549 this.addWidgetToArea("sidebar-button", CustomizableUI.AREA_NAVBAR); 5550 } 5551 CustomizableUI.endBatchUpdate(); 5552 } else { 5553 this.saveNavBarWhenVerticalTabsState(); 5554 // We're switching to vertical in this session; pull saved state from pref and update placements 5555 this.restoreSavedHorizontalTabStripState(); 5556 } 5557 // Give the sidebar a chance to adjust before we show/hide the toolbars 5558 lazy.log.debug("CustomizableUI notifying tabstrip-orientation-change"); 5559 Services.obs.notifyObservers(null, "tabstrip-orientation-change", { 5560 isVertical: toVertical, 5561 }); 5562 5563 this.setToolbarVisibility( 5564 CustomizableUI.AREA_VERTICAL_TABSTRIP, 5565 toVertical 5566 ); 5567 this.setToolbarVisibility(CustomizableUI.AREA_TABSTRIP, !toVertical); 5568 changeWidgetRemovability("tabbrowser-tabs", false); 5569 5570 for (let [win] of gBuildWindows) { 5571 win.TabBarVisibility.update(true); 5572 } 5573 }, 5574 }; 5575 Object.freeze(CustomizableUIInternal); 5576 5577 /** 5578 * This is the publicly exposed interface CustomizableUI. It uses old-school 5579 * encapsulation by forwarding most method calls to CustomizableUIInternal, 5580 * which is not exported. 5581 */ 5582 export var CustomizableUI = { 5583 /** 5584 * Constant reference to the ID of the navigation toolbar. 5585 */ 5586 AREA_NAVBAR: "nav-bar", 5587 /** 5588 * Constant reference to the ID of the menubar's toolbar. 5589 */ 5590 AREA_MENUBAR: "toolbar-menubar", 5591 /** 5592 * Constant reference to the ID of the tabstrip toolbar. 5593 */ 5594 AREA_TABSTRIP: "TabsToolbar", 5595 5596 /** 5597 * Constant reference to the ID of the vertical tabstrip toolbar. 5598 */ 5599 AREA_VERTICAL_TABSTRIP: "vertical-tabs", 5600 5601 /** 5602 * Constant reference to the ID of the bookmarks toolbar. 5603 */ 5604 AREA_BOOKMARKS: "PersonalToolbar", 5605 /** 5606 * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel. 5607 */ 5608 AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list", 5609 /** 5610 * Constant reference to the ID of the addons area. 5611 */ 5612 AREA_ADDONS: "unified-extensions-area", 5613 /** 5614 * Constant reference to the ID of the customization palette, which is 5615 * where widgets go when they're not assigned to an area. Note that this 5616 * area is "virtual" in that it's never set as a value for a widgets 5617 * currentArea or defaultArea. It's only used for the `canWidgetMoveToArea` 5618 * function to check if widgets can be moved to the palette. Callers who 5619 * wish to move items to the palette should use `removeWidgetFromArea`. 5620 */ 5621 AREA_NO_AREA: "customization-palette", 5622 /** 5623 * Constant indicating the area is a panel. 5624 */ 5625 TYPE_PANEL: "panel", 5626 /** 5627 * Constant indicating the area is a toolbar. 5628 */ 5629 TYPE_TOOLBAR: "toolbar", 5630 5631 /** 5632 * Constant indicating a XUL-type provider. 5633 */ 5634 PROVIDER_XUL: "xul", 5635 /** 5636 * Constant indicating an API-type provider. 5637 */ 5638 PROVIDER_API: "api", 5639 /** 5640 * Constant indicating dynamic (special) widgets: spring, spacer, and separator. 5641 */ 5642 PROVIDER_SPECIAL: "special", 5643 5644 /** 5645 * Constant indicating the widget is built-in 5646 */ 5647 SOURCE_BUILTIN: "builtin", 5648 /** 5649 * Constant indicating the widget is externally provided 5650 * (e.g. by add-ons or other items not part of the builtin widget set). 5651 */ 5652 SOURCE_EXTERNAL: "external", 5653 5654 /** 5655 * Constant indicating the reason the event was fired was a window closing 5656 */ 5657 REASON_WINDOW_CLOSED: "window-closed", 5658 /** 5659 * Constant indicating the reason the event was fired was an area being 5660 * unregistered separately from window closing mechanics. 5661 */ 5662 REASON_AREA_UNREGISTERED: "area-unregistered", 5663 5664 /** 5665 * An iteratable property of windows managed by CustomizableUI. 5666 * Note that this can *only* be used as an iterator. ie: 5667 * for (let window of CustomizableUI.windows) { ... } 5668 */ 5669 windows: { 5670 *[Symbol.iterator]() { 5671 for (let [window] of gBuildWindows) { 5672 yield window; 5673 } 5674 }, 5675 }, 5676 5677 get verticalTabsEnabled() { 5678 return lazy.verticalTabsPref; 5679 }, 5680 5681 /** 5682 * Fired when a widget is added to an area. 5683 * 5684 * @callback CustomizableUIOnWidgetAddedCallback 5685 * @param {string} aWidgetId 5686 * The ID of the widget that was added to an area. 5687 * @param {string} aArea 5688 * The ID of the area that the widget was added to. 5689 * @param {number} aPosition 5690 * The position of the widget in the area that it was added to. 5691 */ 5692 5693 /** 5694 * Fired when a widget is moved within its area. 5695 * 5696 * @callback CustomizableUIOnWidgetMovedCallback 5697 * @param {string} aWidgetId 5698 * The ID of the widget that was moved. 5699 * @param {string} aArea 5700 * The ID of the area that the widget was moved within. 5701 * @param {number} aOldPosition 5702 * The original position of the widget before being moved. 5703 * @param {number} aNewPosition 5704 * The new position of the widget after being moved. 5705 */ 5706 5707 /** 5708 * Fired when a widget is removed from an area. 5709 * 5710 * @callback CustomizableUIOnWidgetRemovedCallback 5711 * @param {string} aWidgetId 5712 * The ID of the widget that was removed. 5713 * @param {string} aArea 5714 * The ID of the area that the widget was removed from. 5715 */ 5716 5717 /** 5718 * Fired *before* a widget's DOM node is acted upon by CustomizableUI 5719 * (to add, move or remove it). 5720 * 5721 * @callback CustomizableUIOnWidgetBeforeDOMChange 5722 * @param {Element} aNode 5723 * The DOM node being acted upon. 5724 * @param {Element|null} aNextNode 5725 * The DOM node (if any) before which a widget will be inserted. 5726 * @param {Element} aContainer 5727 * The *actual* DOM container for the widget (could be an overflow panel in 5728 * case of an overflowable toolbar). 5729 * @param {boolean} aWasRemoval 5730 * True iff the action about to happen is the removal of the DOM node. 5731 */ 5732 5733 /** 5734 * Fired *after* a widget's DOM node is acted upon by CustomizableUI 5735 * (to add, move or remove it). 5736 * 5737 * @callback CustomizableUIOnWidgetAfterDOMChange 5738 * @param {Element} aNode 5739 * The DOM node that was acted upon. 5740 * @param {Element|null} aNextNode 5741 * The DOM node (if any) that the widget was inserted before. 5742 * @param {Element} aContainer 5743 * The *actual* DOM container for the widget (could be an overflow panel in 5744 * case of an overflowable toolbar). 5745 * @param {boolean} aWasRemoval 5746 * True iff the action that happened was the removal of the DOM node. 5747 */ 5748 5749 /** 5750 * Fired after a reset to default placements moves a widget's node to a 5751 * different location. 5752 * 5753 * @callback CustomizableUIOnWidgetReset 5754 * @param {Element} aNode 5755 * The DOM node for the widget that was moved. 5756 * @param {Element} aContainer 5757 * The *actual* DOM container for the widget (could be an overflow panel in 5758 * case of an overflowable toolbar) after the reset. (NB: it might already 5759 * have been there and been moved to a different position!) 5760 */ 5761 5762 /** 5763 * Fired after undoing a reset to default placements moves a widget's 5764 * node to a different location. 5765 * 5766 * @callback CustomizableUIOnWidgetUndoMove 5767 * @param {Element} aNode 5768 * The DOM node for the widget that was moved after the undo. 5769 * @param {Element} aContainer 5770 * The *actual* DOM container for the widget (could be an overflow panel in 5771 * case of an overflowable toolbar) after the undo-move. (NB: it might 5772 * already have been there and been moved to a different position!) 5773 */ 5774 5775 /** 5776 * Fired when a widget with id aWidgetId has been created, but before it 5777 * is added to any placements or any DOM nodes have been constructed. 5778 * Only fired for API-based widgets. 5779 * 5780 * @callback CustomizableUIOnWidgetCreated 5781 * @param {string} aWidgetId 5782 * The ID of the widget that was created. 5783 */ 5784 5785 /** 5786 * Fired after a reset to default placements is complete on an area's 5787 * DOM node. Note that this is fired for each DOM node across all windows. 5788 * 5789 * @callback CustomizableUIOnAreaReset 5790 * @param {string} aArea 5791 * The ID for the area that was reset. 5792 * @param {Element} aContainer 5793 * The DOM node for the area that was reset. 5794 */ 5795 5796 /** 5797 * Fired after a widget with id aWidgetId has been created, and has been 5798 * added to either its default area or the area in which it was placed 5799 * previously. If the widget has no default area and/or it has never 5800 * been placed anywhere, aArea may be null. Only fired for API-based 5801 * widgets. 5802 * 5803 * @callback CustomizableUIOnWidgetAfterCreation 5804 * @param {string} aWidgetId 5805 * The ID of the widget that was just created. 5806 * @param {string|null} aArea 5807 * The ID of the area that the widget was placed in, or null if it is 5808 * now in the customization palette. 5809 */ 5810 5811 /** 5812 * Fired when a widget is destroyed. Only fired for API-based widgets. 5813 * 5814 * @callback CustomizableUIOnWidgetDestroyed 5815 * @param {string} aWidgetId 5816 * The ID of the widget that was destroyed. 5817 */ 5818 5819 /** 5820 * Fired when a window is unloaded and a widget's instance is destroyed 5821 * because of this. Only fired for API-based widgets. 5822 * 5823 * @callback CustomizableUIOnWidgetInstanceRemoved 5824 * @param {string} aWidgetId 5825 * The ID of the widget that was just removed. 5826 * @param {Document} aDocument 5827 * The Document that the widget belonged to that was just unloaded. 5828 */ 5829 5830 /** 5831 * Fired when entering customize mode in aWindow. 5832 * 5833 * @callback CustomizableUIOnCustomizeStart 5834 * @param {DOMWindow} aWindow 5835 * The window in which customize mode was entered. 5836 */ 5837 5838 /** 5839 * Fired when exiting customize mode in aWindow. 5840 * 5841 * @callback CustomizableUIOnCustomizeEnd 5842 * @param {DOMWindow} aWindow 5843 * The window in which customize mode was exited. 5844 */ 5845 5846 /** 5847 * Fired when a widget's DOM node is overflowing its toolbar and will be 5848 * displayed in an overflow panel. 5849 * 5850 * @callback CustomizableUIOnWidgetOverflow 5851 * @param {Element} aNode 5852 * The DOM node for the widget that overflowed. 5853 * @param {Element} aContainer 5854 * The DOM container that the widget just overflowed out of. 5855 */ 5856 5857 /** 5858 * Fired when a widget that was overflowed out of its toolbar container 5859 * "underflows" back. 5860 * 5861 * @callback CustomizableUIOnWidgetUnderflow 5862 * @param {Element} aNode 5863 * The DOM node for the widget that had overflowed out. 5864 * @param {Element} aContainer 5865 * The DOM container that the widget is underflowing back into. 5866 */ 5867 5868 /** 5869 * Fired when a window has been opened that is managed by CustomizableUI, 5870 * once all of the prerequisite setup has been done. 5871 * 5872 * @callback CustomizableUIOnWindowOpened 5873 * @param {DOMWindow} aWindow 5874 * The window that opened. 5875 */ 5876 5877 /** 5878 * Fired when a window that has been managed by CustomizableUI has been 5879 * closed. 5880 * 5881 * @callback CustomizableUIOnWindowClosed 5882 * @param {DOMWindow} aWindow 5883 * The window that closed. 5884 */ 5885 5886 /** 5887 * Fired after an area node is first built when it is registered. This is 5888 * often when the window has opened, but in the case of add-ons, could fire 5889 * when the node has just been registered with CustomizableUI after an add-on 5890 * update or disable/enable sequence. 5891 * 5892 * @callback CustomizableUIOnAreaNodeRegistered 5893 * @param {string} aArea 5894 * The ID for the area that was just registered. 5895 * @param {Element} aContainer 5896 * The DOM node for the customizable area. 5897 */ 5898 5899 /** 5900 * Fired when an area node is explicitly unregistered by an API caller, or by 5901 * a window closing. The aReason parameter indicates which of these is the 5902 * case. 5903 * 5904 * @callback CustomizableUIOnAreaNodeUnregistered 5905 * @param {string} aArea 5906 * The ID for the area that was just registered. 5907 * @param {Element} aContainer 5908 * The DOM node for the customizable area. 5909 * @param {string} aReason 5910 * One of either CustomizableUI.REASON_WINDOW_CLOSED or 5911 * CustomizableUI.REASON_AREA_UNREGISTERED. 5912 */ 5913 5914 /** 5915 * @typedef {object} CustomizableUIListener 5916 * @property {CustomizableUIOnWidgetAddedCallback} [onWidgetAdded] 5917 * @property {CustomizableUIOnWidgetMovedCallback} [onWidgetMoved] 5918 * @property {CustomizableUIOnWidgetRemovedCallback} [onWidgetRemoved] 5919 * @property {CustomizableUIOnWidgetBeforeDOMChange} [onWidgetBeforeDOMChange] 5920 * @property {CustomizableUIOnWidgetAfterDOMChange} [onWidgetAfterDOMChange] 5921 * @property {CustomizableUIOnWidgetReset} [onWidgetReset] 5922 * @property {CustomizableUIOnWidgetUndoMove} [onWidgetUndoMove] 5923 * @property {CustomizableUIOnWidgetCreated} [onWidgetCreated] 5924 * @property {CustomizableUIOnAreaReset} [onAreaReset] 5925 * @property {CustomizableUIOnWidgetAfterCreation} [onWidgetAfterCreation] 5926 * @property {CustomizableUIOnWidgetDestroyed} [onWidgetDestroyed] 5927 * @property {CustomizableUIOnWidgetInstanceRemoved} [onWidgetInstanceRemoved] 5928 * @property {CustomizableUIOnWidgetDrag} [onWidgetDrag] 5929 * @property {CustomizableUIOnCustomizeStart} [onCustomizeStart] 5930 * @property {CustomizableUIOnCustomizeEnd} [onCustomizeEnd] 5931 * @property {CustomizableUIOnWidgetOverflow} [onWidgetOverflow] 5932 * @property {CustomizableUIOnWindowOpened} [onWindowOpened] 5933 * @property {CustomizableUIOnWindowClosed} [onWindowClosed] 5934 * @property {CustomizableUIOnAreaNodeRegistered} [onAreaNodeRegistered] 5935 * @property {CustomizableUIOnAreaNodeUnregistered} [onAreaNodeUnregistered] 5936 */ 5937 5938 /** 5939 * Add a listener object that will get fired for various events regarding 5940 * window, area, and window lifetimes / events, as well as customization 5941 * events. 5942 * 5943 * @param {CustomizableUIListener} aListener 5944 * The listener object to add. Not all event handler methods need to be 5945 * defined. CustomizableUI will catch exceptions. Events are dispatched 5946 * synchronously on the UI thread, so if you can delay any/some of your 5947 * processing, that is advisable. 5948 */ 5949 addListener(aListener) { 5950 CustomizableUIInternal.addListener(aListener); 5951 }, 5952 5953 /** 5954 * Remove a listener that was previously added with addListener. 5955 * 5956 * @param {CustomizableUIListener} aListener 5957 * The listener object to remove. 5958 */ 5959 removeListener(aListener) { 5960 CustomizableUIInternal.removeListener(aListener); 5961 }, 5962 5963 /** 5964 * Register a customizable area with CustomizableUI. 5965 * 5966 * @param {string} aName 5967 * The name of the area to register. Can only contain alphanumeric 5968 * characters, dashes (-) and underscores (_). 5969 * @param {object} aProperties 5970 * The properties of the area to register. 5971 * @param {string} [aProperties.type=CustomizableUI.TYPE_TOOLBAR] 5972 * The type of area being registered. Either CustomizableUI.TYPE_TOOLBAR 5973 * (default) or CustomizableUI.TYPE_PANEL. 5974 * @param {Element|undefined} [aProperties.anchor] 5975 * For a menu panel or overflowable toolbar area, the anchoring node for the 5976 * panel. 5977 * @param {boolean} [aProperties.overflowable] 5978 * Set to true if your toolbar is overflowable. This requires an anchor, and 5979 * only has an effect for toolbars. 5980 * @param {string[]} [aProperties.defaultPlacements] 5981 * An array of widget IDs making up the default contents of the area. 5982 * @param {boolean|null} [aProperties.defaultCollapsed=true] 5983 * (INTERNAL ONLY) applies if the type is CustomizableUI.TYPE_TOOLBAR, 5984 * specifies if the toolbar is collapsed by default (defaults to true). 5985 * Specify `null` to ensure that reset/inDefaultArea don't care 5986 * about a toolbar's collapsed state 5987 */ 5988 registerArea(aName, aProperties) { 5989 CustomizableUIInternal.registerArea(aName, aProperties); 5990 }, 5991 /** 5992 * Register a concrete node for a registered area. This method needs to be called 5993 * with any toolbar in the main browser window that has its "customizable" attribute 5994 * set to true. 5995 * 5996 * Note that ideally, you should register your toolbar using registerArea 5997 * before calling this. If you don't, the node will be saved for processing when 5998 * you call registerArea. Note that CustomizableUI won't restore state in the area, 5999 * allow the user to customize it in customize mode, or otherwise deal 6000 * with it, until the area has been registered. 6001 * 6002 * @param {Element} aToolbar 6003 * The <xul:toolbar> node to register. 6004 */ 6005 registerToolbarNode(aToolbar) { 6006 CustomizableUIInternal.registerToolbarNode(aToolbar); 6007 }, 6008 /** 6009 * Register a panel node. A panel treated slightly differently from a toolbar in 6010 * terms of what items can be moved into it. For example, a panel cannot have a 6011 * spacer or a spring put into it. 6012 * 6013 * @param {Element} aNode 6014 * The panel contents DOM node being registered. 6015 * @param {string} aArea 6016 * The name of the area for which to register this node. 6017 */ 6018 registerPanelNode(aNode, aArea) { 6019 CustomizableUIInternal.registerPanelNode(aNode, aArea); 6020 }, 6021 /** 6022 * Unregister a customizable area. The inverse of registerArea. 6023 * 6024 * Unregistering an area will remove all the (removable) widgets in the 6025 * area, which will return to the panel, and destroy all other traces 6026 * of the area within CustomizableUI. Note that this means the *contents* 6027 * of the area's DOM nodes will be moved to the panel or removed, but 6028 * the area's DOM nodes *themselves* will stay. 6029 * 6030 * Furthermore, by default the placements of the area will be kept in the 6031 * saved state (!) and restored if you re-register the area at a later 6032 * point. This is useful for e.g. add-ons that get disabled and then 6033 * re-enabled (e.g. when they update). 6034 * 6035 * You can override this last behaviour (and destroy the placements 6036 * information in the saved state) by passing true for aDestroyPlacements. 6037 * 6038 * @param {string} aName 6039 * The name of the area to unregister. 6040 * @param {boolean} [aDestroyPlacements] 6041 * True if the placements information for the area should be destroyed 6042 * too. Defaults to not destroying the placements information. 6043 */ 6044 unregisterArea(aName, aDestroyPlacements) { 6045 CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements); 6046 }, 6047 /** 6048 * Add a widget to an area. 6049 * If the area to which you try to add is not known to CustomizableUI, 6050 * this will throw. 6051 * If the area to which you try to add is the same as the area in which 6052 * the widget is currently placed, this will do the same as 6053 * moveWidgetWithinArea. 6054 * If the widget cannot be removed from its original location, this will 6055 * no-op. 6056 * 6057 * This will fire an onWidgetAdded notification, 6058 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification 6059 * for each window CustomizableUI knows about. 6060 * 6061 * @param {string} aWidgetId 6062 * The ID of the widget to add to the area. 6063 * @param {string} aArea 6064 * The name of the area to add the widget to. 6065 * @param {number} [aPosition] 6066 * The position at which to add the widget. If you do not pass a position, 6067 * the widget will be added to the end of the area. 6068 */ 6069 addWidgetToArea(aWidgetId, aArea, aPosition) { 6070 CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition); 6071 }, 6072 /** 6073 * Remove a widget from its area. If the widget cannot be removed from its 6074 * area, or is not in any area, this will no-op. Otherwise, this will fire an 6075 * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and 6076 * onWidgetAfterDOMChange notification for each window CustomizableUI knows 6077 * about. 6078 * 6079 * @param {string} aWidgetId 6080 * The ID of the widget to remove from its area. 6081 */ 6082 removeWidgetFromArea(aWidgetId) { 6083 CustomizableUIInternal.removeWidgetFromArea(aWidgetId); 6084 }, 6085 /** 6086 * Move a widget within an area. 6087 * If the widget is not in any area, this will no-op. 6088 * If the widget is already at the indicated position, this will no-op. 6089 * 6090 * Otherwise, this will move the widget and fire an onWidgetMoved notification, 6091 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for 6092 * each window CustomizableUI knows about. 6093 * 6094 * @param {string} aWidgetId 6095 * The ID of the widget to move. 6096 * @param {number} aPosition 6097 * The position to move the widget to. Negative values or values greater 6098 * than the number of widgets will be interpreted to mean moving the widget 6099 * to respectively the first or last position. 6100 */ 6101 moveWidgetWithinArea(aWidgetId, aPosition) { 6102 CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition); 6103 }, 6104 /** 6105 * Ensure a XUL-based widget created in a window after areas were 6106 * initialized moves to its correct position. 6107 * Always prefer this over moving items in the DOM yourself. 6108 * 6109 * NB: why is this API per-window, you wonder? Because if you need this, 6110 * presumably you yourself need to create the widget in all the windows 6111 * and need to loop through them anyway. 6112 * 6113 * @param {string} aWidgetId 6114 * The ID of the widget that was just created. 6115 * @param {DOMWindow} aWindow 6116 * The window in which you want to ensure it was added. 6117 * @returns {boolean} 6118 * True if the widget was successfully placed in the window (or was already 6119 * placed in the window). False if something goes wrong with checking for 6120 * the presence of the widget in the window. 6121 */ 6122 ensureWidgetPlacedInWindow(aWidgetId, aWindow) { 6123 return CustomizableUIInternal.ensureWidgetPlacedInWindow( 6124 aWidgetId, 6125 aWindow 6126 ); 6127 }, 6128 /** 6129 * Start a batch update of items. 6130 * During a batch update, the customization state is not saved to the user's 6131 * preferences file, in order to reduce (possibly sync) IO. 6132 * Calls to begin/endBatchUpdate may be nested. 6133 * 6134 * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once 6135 * for each call to beginBatchUpdate, even if there are exceptions in the 6136 * code in the batch update. Otherwise, for the duration of the 6137 * Firefox session, customization state is never saved. Typically, you 6138 * would do this using a try...finally block. 6139 */ 6140 beginBatchUpdate() { 6141 CustomizableUIInternal.beginBatchUpdate(); 6142 }, 6143 /** 6144 * End a batch update. See the documentation for beginBatchUpdate above. 6145 * 6146 * State is not saved if we believe it is identical to the last known 6147 * saved state. State is only ever saved when all batch updates have 6148 * finished (ie there has been 1 endBatchUpdate call for each 6149 * beginBatchUpdate call). If any of the endBatchUpdate calls pass 6150 * aForceDirty=true, we will flush to the prefs file. 6151 * 6152 * @param {boolean} [aForceDirty=false] 6153 * Force CustomizableUI to flush to the prefs file when all batch updates 6154 * have finished. Defaults to false. 6155 */ 6156 endBatchUpdate(aForceDirty = false) { 6157 CustomizableUIInternal.endBatchUpdate(aForceDirty); 6158 }, 6159 6160 /** 6161 * A function that will be invoked with the document in which to build a 6162 * widget. Should return the DOM node that has been constructed. 6163 * 6164 * @callback CustomizableUICreateWidgetOnBuild 6165 * @param {Document} aDoc 6166 * The document to create the widget in. 6167 * @returns {Element} 6168 * The DOM node that was constructed for the widget. 6169 */ 6170 6171 /** 6172 * Invoked before the widget gets a DOM node constructed for it, passing the 6173 * document in which that will happen. This is useful especially for 'view' 6174 * type widgets that need to construct their views on the fly (e.g. from 6175 * bootstrapped add-ons). If the function returns `false`, the widget will 6176 * not be created. 6177 * 6178 * @callback CustomizableUICreateWidgetOnBeforeCreated 6179 * @param {Document} aDoc 6180 * The document that the widget might be created in. 6181 * @returns {boolean} 6182 * True if the widget should be created. 6183 */ 6184 6185 /** 6186 * A function that will be invoked whenever the widget has a DOM node 6187 * constructed, passing the constructed node as an argument. 6188 * 6189 * @callback CustomizableUICreateWidgetOnCreated 6190 * @param {Element} aNode 6191 * The DOM node that was constructed for the widget in a document. 6192 */ 6193 6194 /** 6195 * A function that will be invoked after the widget has a DOM node destroyed, 6196 * passing the document from which it was removed. This is useful especially 6197 * for 'view' type widgets that need to cleanup after views that were 6198 * constructed on the fly. 6199 * 6200 * @callback CustomizableUICreateWidgetOnDestroyed 6201 * @param {Document} aDoc 6202 * The document that the widget was destroyed in. 6203 */ 6204 6205 /** 6206 * A function that will be invoked when the user activates the button but 6207 * before the command is evaluated. Useful if code needs to run to change the 6208 * button's icon in preparation to the pending command action. Called for any 6209 * type that supports the handler. The command type, either "view" or 6210 * "command", may be returned to force the action that will occur. View will 6211 * open the panel and command will result in calling onCommand. 6212 * 6213 * @callback CustomizableUICreateWidgetOnBeforeCommand 6214 * @param {Event} aEvent 6215 * The command event that occurred on the button. 6216 * @param {Element} aNode 6217 * The element upon which the command event occurred. 6218 * @returns {string} 6219 * One of "action" or "view". 6220 */ 6221 6222 /** 6223 * Useful for custom, button and button-and-view widgets; a function that will 6224 * be invoked when the user activates the button. 6225 * 6226 * @callback CustomizableUICreateWidgetOnCommand 6227 * @param {Event} aEvent 6228 * The command event that was fired for the node. 6229 */ 6230 6231 /** 6232 * A function that will be invoked when the user clicks a widget node. 6233 * 6234 * @callback CustomizableUICreateWidgetOnClick 6235 * @param {Event} aEvent 6236 * The click event that was fired for the widget node. 6237 */ 6238 6239 /** 6240 * A function that will be invoked when a user shows your view. If any event 6241 * handler calls aEvt.preventDefault(), the view will not be shown. 6242 * 6243 * The event's `detail` property is an object with an `addBlocker` method. 6244 * Handlers which need to perform asynchronous operations before the view is 6245 * shown may pass this method a Promise, which will prevent the view from 6246 * showing until it resolves. Additionally, if the promise resolves to the 6247 * exact value `false`, the view will not be shown. 6248 * 6249 * @callback CustomizableUICreateWidgetOnViewShowing 6250 * @param {Event} aEvent 6251 * The ViewShowing event. See PanelMultiView.sys.mjs. 6252 */ 6253 6254 /** 6255 * A function that will be invoked when a user hides your view. 6256 * 6257 * @callback CustomizableUICreateWidgetOnViewHiding 6258 * @param {Event} aEvent 6259 * The ViewHiding event. See PanelMultiView.sys.mjs. 6260 */ 6261 6262 /** 6263 * @typedef {object} CustomizableUICreateWidgetProperties 6264 * @property {string} id 6265 * The ID of the widget to be created. 6266 * @property {string} [type="button"] 6267 * The type of widget to create. The valid types are: 6268 * 'button' - for simple button widgets (the default) 6269 * 'view' - for buttons that open a panel or subview, 6270 * depending on where they are placed. 6271 * 'button-and-view' - A combination of 'button' and 'view', 6272 * which looks different depending on whether it's 6273 * located in the toolbar or in the panel: When 6274 * located in the toolbar, the widget is shown as 6275 * a combined item of a button and a dropmarker 6276 * button. The button triggers the command and the 6277 * dropmarker button opens the view. When located 6278 * in the panel, shown as one item which opens the 6279 * view, and the button command cannot be 6280 * triggered separately. 6281 * 'custom' - for fine-grained control over the creation 6282 * of the widget. 6283 * @property {string} [viewId] 6284 * Only useful for views and button-and-view widgets (and required in those 6285 * cases). Should be set to the id of the <panelview> that should be shown 6286 * when clicking the widget. If used with a custom widget, the widget must 6287 * also provide a toolbaritem where the first child is the view button. 6288 * @property {CustomizableUICreateWidgetOnBuild} [onBuild] 6289 * Only useful for custom widgets (and required there). 6290 * @property {CustomizableUICreateWidgetOnBeforeCreated} [onBeforeCreated] 6291 * Called for all button and non-custom widgets. 6292 * @property {CustomizableUICreateWidgetOnCreated} [onCreated] 6293 * @property {CustomizableUICreateWidgetOnDestroyed} [onDestroyed] 6294 * @property {CustomizableUICreateWidgetOnBeforeCommand} [onBeforeCommand] 6295 * @property {CustomizableUICreateWidgetOnCommand} [onCommand] 6296 * @property {CustomizableUICreateWidgetOnClick} [onClick] 6297 * @property {CustomizableUICreateWidgetOnViewShowing} [onViewShowing] 6298 * Only useful for view and button-and-view widgets. 6299 * @property {CustomizableUICreateWidgetOnViewHiding} [onViewHiding] 6300 * Only useful for view and button-and-view widgets. 6301 * @property {string} [l10nId] 6302 * A Fluent string identifier to use for localizing attributes on the 6303 * widget. If present, preferred over the label/tooltiptext parameters. 6304 * @property {string} [tooltiptext] 6305 * **Deprecated** - use l10nId and Fluent instead. A string to use for the 6306 * tooltip of the widget. 6307 * @property {string} [label] 6308 * **Deprecated** - use l10nId and Fluent instead. A string to use for the 6309 * label of the widget. 6310 * @property {string} [localized] 6311 * **Deprecated** - use l10nId and Fluent instead. If true, or undefined, 6312 * attempt to retrieve the widget's string properties from the customizable 6313 * widgets string bundle. 6314 * @property {boolean} [removable=true] 6315 * Whether the widget can be removed from a customizable area. 6316 * Note: if you specify false here, you must provide a defaultArea, too. 6317 * @property {boolean} [overflows=true] 6318 * Whether widget can overflow when placed within an overflowable toolbar. 6319 * @property {string} [defaultArea] 6320 * The default area to add the widget to. If not supplied, this widget will 6321 * be placed in the palette by default. A valid default area is required if 6322 * the widget is not removable. 6323 * @property {string} [shortcutId] 6324 * The id of an element that has a shortcut for this widget. This is only 6325 * used to display the shortcut as part of the tooltip for builtin widgets 6326 * (which have strings inside customizableWidgets.properties). If you're in 6327 * an add-on, you should not set this property. If l10nId is provided, the 6328 * resulting shortcut is passed as the "$shortcut" variable to the Fluent 6329 * message. 6330 * @property {boolean} [showInPrivateBrowsing=true] 6331 * True to show the widget in private browsing mode windows. 6332 * @property {boolean} [hideInNonPrivateBrowsing=false] 6333 * True to hide the widget in non-private browsing mode windows. 6334 * @property {boolean} [tabSpecific] 6335 * True to close any widget view panels if the selected tab changes. 6336 * @property {boolean} [locationSpecific] 6337 * True to close any widget view panels if the location changes. 6338 * @property {boolean} [webExtension] 6339 * True if this widget is being created on behalf of a WebExtension. 6340 */ 6341 6342 /** 6343 * Create a widget. 6344 * 6345 * To create a widget, you should pass an object with its desired 6346 * properties. 6347 * 6348 * @param {CustomizableUICreateWidgetProperties} aProperties 6349 * The properties for the widget to be created. 6350 * @returns {WidgetGroupWrapper|XULWidgetGroupWrapper} 6351 */ 6352 createWidget(aProperties) { 6353 return CustomizableUIInternal.wrapWidget( 6354 CustomizableUIInternal.createWidget(aProperties) 6355 ); 6356 }, 6357 /** 6358 * Destroy a widget 6359 * 6360 * If the widget is part of the default placements in an area, this will 6361 * remove it from there. It will also remove any DOM instances. However, 6362 * it will keep the widget in the placements for whatever area it was 6363 * in at the time. You can remove it from there yourself by calling 6364 * CustomizableUI.removeWidgetFromArea(aWidgetId). 6365 * 6366 * @param {string} aWidgetId 6367 * The ID of the widget to destroy. 6368 */ 6369 destroyWidget(aWidgetId) { 6370 CustomizableUIInternal.destroyWidget(aWidgetId); 6371 }, 6372 /** 6373 * Get a wrapper object with information about the widget. 6374 * The object provides the following properties 6375 * (all read-only unless otherwise indicated): 6376 * 6377 * - id: the widget's ID; 6378 * - type: the type of widget (button, view, custom). For 6379 * XUL-provided widgets, this is always 'custom'; 6380 * - provider: the provider type of the widget, id est one of 6381 * PROVIDER_API or PROVIDER_XUL; 6382 * - forWindow(w): a method to obtain a single window wrapper for a widget, 6383 * in the window w passed as the only argument; 6384 * - instances: an array of all instances (single window wrappers) 6385 * of the widget. This array is NOT live; 6386 * - areaType: the type of the widget's current area 6387 * - isGroup: true; will be false for wrappers around single widget nodes; 6388 * - source: for API-provided widgets, whether they are built-in to 6389 * Firefox or add-on-provided; 6390 * - disabled: for API-provided widgets, whether the widget is currently 6391 * disabled. NB: this property is writable, and will toggle 6392 * all the widgets' nodes' disabled states; 6393 * - label: for API-provied widgets, the label of the widget; 6394 * - tooltiptext: for API-provided widgets, the tooltip of the widget; 6395 * - showInPrivateBrowsing: for API-provided widgets, whether the widget is 6396 * visible in private browsing; 6397 * - hideInNonPrivateBrowsing: for API-provided widgets, whether the widget is 6398 * hidden in non-private browsing; 6399 * 6400 * Single window wrappers obtained through forWindow(someWindow) or from the 6401 * instances array have the following properties 6402 * (all read-only unless otherwise indicated): 6403 * 6404 * - id: the widget's ID; 6405 * - type: the type of widget (button, view, custom). For 6406 * XUL-provided widgets, this is always 'custom'; 6407 * - provider: the provider type of the widget, id est one of 6408 * PROVIDER_API or PROVIDER_XUL; 6409 * - node: reference to the corresponding DOM node; 6410 * - anchor: the anchor on which to anchor panels opened from this 6411 * node. This will point to the overflow chevron on 6412 * overflowable toolbars if and only if your widget node 6413 * is overflowed, to the anchor for the panel menu 6414 * if your widget is inside the panel menu, and to the 6415 * node itself in all other cases; 6416 * - overflowed: boolean indicating whether the node is currently in the 6417 * overflow panel of the toolbar; 6418 * - isGroup: false; will be true for the group widget; 6419 * - label: for API-provided widgets, convenience getter for the 6420 * label attribute of the DOM node; 6421 * - tooltiptext: for API-provided widgets, convenience getter for the 6422 * tooltiptext attribute of the DOM node; 6423 * - disabled: for API-provided widgets, convenience getter *and setter* 6424 * for the disabled state of this single widget. Note that 6425 * you may prefer to use the group wrapper's getter/setter 6426 * instead. 6427 * 6428 * @param {string} aWidgetId 6429 * The ID of the widget whose information you need. 6430 * @returns {WidgetGroupWrapper|XULWidgetGroupWrapper|null} 6431 * A wrapper around the widget as described above, or null if the widget is 6432 * known not to exist (anymore). NB: A non-null return is no guarantee the 6433 * widget exists because we cannot know in advance if a XUL widget exists or 6434 * not. 6435 */ 6436 getWidget(aWidgetId) { 6437 return CustomizableUIInternal.wrapWidget(aWidgetId); 6438 }, 6439 /** 6440 * Get an array of widget wrappers (see getWidget) for all the widgets 6441 * which are currently not in any area (so which are in the palette). 6442 * 6443 * @param {DOMElement} aWindowPalette 6444 * The palette element (and by extension, the window) in which 6445 * CustomizableUI should look. This matters because of course XUL-provided 6446 * widgets could be available in some windows but not others, and likewise 6447 * API-provided widgets might not exist in a private window (because of the 6448 * showInPrivateBrowsing property). 6449 * @returns {Array<WidgetGroupWrapper|XULWidgetGroupWrapper>} 6450 * An array of widget wrappers (see getWidget) 6451 */ 6452 getUnusedWidgets(aWindowPalette) { 6453 return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map( 6454 CustomizableUIInternal.wrapWidget, 6455 CustomizableUIInternal 6456 ); 6457 }, 6458 /** 6459 * Get an array of all the widget IDs placed in an area. 6460 * Modifying the array will not affect CustomizableUI. 6461 * 6462 * NB: will throw if called too early (before placements have been fetched) 6463 * or if the area is not currently known to CustomizableUI. 6464 * 6465 * @param {string} aArea 6466 * The name of the area whose placements you want to obtain. 6467 * @returns {string[]} 6468 * An array containing the widget IDs that are in the area. 6469 */ 6470 getWidgetIdsInArea(aArea) { 6471 if (!gAreas.has(aArea)) { 6472 throw new Error("Unknown customization area: " + aArea); 6473 } 6474 if (!gPlacements.has(aArea)) { 6475 throw new Error(`Area ${aArea} not yet restored`); 6476 } 6477 6478 // We need to clone this, as we don't want to let consumers muck with placements 6479 return [...gPlacements.get(aArea)]; 6480 }, 6481 /** 6482 * Get an array of all the widget IDs in the default placements for an area. 6483 * Modifying the array will not affect CustomizableUI. 6484 * 6485 * @param {string} aArea 6486 * The ID of the area whose default placements you want to obtain. 6487 * @returns {string[]} 6488 * An array containing the widget IDs that are in the default placements for 6489 * that area. 6490 */ 6491 getDefaultPlacementsForArea(aArea) { 6492 return [...gAreas.get(aArea).get("defaultPlacements")]; 6493 }, 6494 /** 6495 * Get an array of widget wrappers for all the widgets in an area. This is 6496 * the same as calling getWidgetIdsInArea and .map() ing the result through 6497 * CustomizableUI.getWidget. Careful: this means that if there are IDs in there 6498 * which don't have corresponding DOM nodes, there might be nulls in this array, 6499 * or items for which wrapper.forWindow(win) will return null. 6500 * 6501 * @param {string} aArea 6502 * The ID of the area whose widgets you want to obtain. 6503 * @returns {Array<WidgetGroupWrapper|XULWidgetGroupWrapper|null>} 6504 * An array of widget wrappers and/or null values for the widget IDs 6505 * placed in an area. 6506 * 6507 * NB: will throw if called too early (before placements have been fetched) 6508 * or if the area is not currently known to CustomizableUI. 6509 */ 6510 getWidgetsInArea(aArea) { 6511 return this.getWidgetIdsInArea(aArea).map( 6512 CustomizableUIInternal.wrapWidget, 6513 CustomizableUIInternal 6514 ); 6515 }, 6516 6517 /** 6518 * Ensure the customizable widget that matches up with this view node 6519 * will get the right subview showing/shown/hiding/hidden events when 6520 * they fire. 6521 * 6522 * @param {Element} aViewNode 6523 * The view node to add listeners to if they haven't been added already. 6524 */ 6525 ensureSubviewListeners(aViewNode) { 6526 return CustomizableUIInternal.ensureSubviewListeners(aViewNode); 6527 }, 6528 /** 6529 * Obtain an array of all the area IDs known to CustomizableUI. 6530 * This array is created for you, so is modifiable without CustomizableUI 6531 * being affected. 6532 */ 6533 get areas() { 6534 return [...gAreas.keys()]; 6535 }, 6536 /** 6537 * Check what kind of area (toolbar or menu panel) an area is. This is 6538 * useful if you have a widget that needs to behave differently depending 6539 * on its location. Note that widget wrappers have a convenience getter 6540 * property (areaType) for this purpose. 6541 * 6542 * @param {string} aArea 6543 * The ID of the area whose type you want to know 6544 * @returns {string} 6545 * Returns CustomizableUI.TYPE_TOOLBAR or CustomizableUI.TYPE_PANEL 6546 * depending on the area, null if the area is unknown. 6547 */ 6548 getAreaType(aArea) { 6549 let area = gAreas.get(aArea); 6550 return area ? area.get("type") : null; 6551 }, 6552 /** 6553 * Check if a toolbar is collapsed by default. 6554 * 6555 * @param {string} aArea 6556 * The ID of the area whose default-collapsed state you want to know. 6557 * @returns {boolean} 6558 * Returns true if the toolbar area is collapsed by default, false if 6559 * not collapsed by default, and null if the area is unknown its collapsed 6560 * state cannot normally be controlled by the user. 6561 */ 6562 isToolbarDefaultCollapsed(aArea) { 6563 let area = gAreas.get(aArea); 6564 return area ? area.get("defaultCollapsed") : null; 6565 }, 6566 /** 6567 * Obtain the DOM node that is the customize target for an area in a 6568 * specific window. 6569 * 6570 * Areas can have a customization target that does not correspond to the 6571 * node itself. In particular, toolbars that have a customizationtarget 6572 * attribute set will have their customization target set to that node. 6573 * This means widgets will end up in the customization target, not in the 6574 * DOM node with the ID that corresponds to the area ID. This is useful 6575 * because it lets you have fixed content in a toolbar (e.g. the panel 6576 * menu item in the navbar) and have all the customizable widgets use 6577 * the customization target. 6578 * 6579 * Using this API yourself is discouraged; you should generally not need 6580 * to be asking for the DOM container node used for a particular area. 6581 * In particular, if you're wanting to check it in relation to a widget's 6582 * node, your DOM node might not be a direct child of the customize target 6583 * in a window if, for instance, the window is in customization mode, or if 6584 * this is an overflowable toolbar and the widget has been overflowed. 6585 * 6586 * @param {string} aArea 6587 * The ID of the area whose customize target you want to have 6588 * @param {DOMWindow} aWindow 6589 * The window where you want to fetch the DOM node. 6590 * @returns {Element} 6591 * The customize target DOM node for aArea in aWindow 6592 */ 6593 getCustomizeTargetForArea(aArea, aWindow) { 6594 return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow); 6595 }, 6596 /** 6597 * Reset the customization state back to its default. 6598 * 6599 * This is the nuclear option. You should never call this except if the user 6600 * explicitly requests it. Firefox does this when the user clicks the 6601 * "Restore Defaults" button in customize mode. 6602 */ 6603 reset() { 6604 CustomizableUIInternal.reset(); 6605 }, 6606 6607 /** 6608 * Undo the previous reset, can only be called immediately after a reset. 6609 * 6610 * @returns {Promise<undefined>} 6611 * A promise that will be resolved when the operation is complete. 6612 */ 6613 undoReset() { 6614 CustomizableUIInternal.undoReset(); 6615 }, 6616 6617 /** 6618 * Remove a custom toolbar added in a previous version of Firefox or using 6619 * an add-on. NB: only works on the customizable toolbars generated by 6620 * the toolbox itself. Intended for use from CustomizeMode, not by 6621 * other consumers. 6622 * 6623 * @param {string} aToolbarId 6624 * The ID of the toolbar to remove. 6625 */ 6626 removeExtraToolbar(aToolbarId) { 6627 CustomizableUIInternal.removeExtraToolbar(aToolbarId); 6628 }, 6629 6630 /** 6631 * Can the last Restore Defaults operation be undone. 6632 * 6633 * @returns {boolean} 6634 * True if the last Restore Defaults operation can be undone. 6635 */ 6636 get canUndoReset() { 6637 return ( 6638 gUIStateBeforeReset.uiCustomizationState != null || 6639 gUIStateBeforeReset.drawInTitlebar != null || 6640 gUIStateBeforeReset.currentTheme != null || 6641 gUIStateBeforeReset.autoTouchMode != null || 6642 gUIStateBeforeReset.uiDensity != null || 6643 gUIStateBeforeReset.sidebarPositionStart != null 6644 ); 6645 }, 6646 6647 /** 6648 * @typedef {object} CustomizableUIPlacementInfo 6649 * @param {string} area 6650 * The ID of the area where the widget is placed. 6651 * @param {number} position 6652 * The 0-indexed position of the widget according to the placements area 6653 * of the area that it's in. 6654 */ 6655 6656 /** 6657 * Get the placement of a widget. This is by far the best way to obtain 6658 * information about what the state of your widget is. The internals of 6659 * this call are cheap (no DOM necessary) and you will know where the user 6660 * has put your widget. 6661 * 6662 * @param {string} aWidgetId 6663 * The ID of the widget whose placement you want to know. 6664 * @param {boolean} [aOnlyRegistered=true] 6665 * Set to false to return placements for widgets that aren't registered, 6666 * but still exist within the placements state, having been registered and 6667 * placed in the past. 6668 * @param {boolean} [aDeadAreas=false] 6669 * Set to true to include placements within "dead" areas that are no longer 6670 * registered, but still exist in the placement state. 6671 * @returns {CustomizableUIPlacementInfo|null} 6672 * Returns null if the widget is not placed anywhere (ie in the palette). 6673 */ 6674 getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) { 6675 return CustomizableUIInternal.getPlacementOfWidget( 6676 aWidgetId, 6677 aOnlyRegistered, 6678 aDeadAreas 6679 ); 6680 }, 6681 /** 6682 * Check if a widget can be removed from the area it's in. 6683 * 6684 * Note that if you're wanting to move the widget somewhere, you should 6685 * generally be checking canWidgetMoveToArea, because that will return 6686 * true if the widget is already in the area where you want to move it (!). 6687 * 6688 * NB: oh, also, this method might lie if the widget in question is a 6689 * XUL-provided widget and there are no windows open, because it 6690 * can obviously not check anything in this case. It will return 6691 * true. You will be able to move the widget elsewhere. However, 6692 * once the user reopens a window, the widget will move back to its 6693 * 'proper' area automagically. 6694 * 6695 * @param {string} aWidgetId 6696 * A widget ID or DOM node to check. 6697 * @returns {boolean} 6698 * True if the widget can be removed from its area. 6699 */ 6700 isWidgetRemovable(aWidgetId) { 6701 return CustomizableUIInternal.isWidgetRemovable(aWidgetId); 6702 }, 6703 /** 6704 * Check if a widget can be moved to a particular area. Like 6705 * isWidgetRemovable but better, because it'll return true if the widget 6706 * is already in the right area. 6707 * 6708 * @param {string} aWidgetId 6709 * The ID of the widget that you want to move somewhere. 6710 * @param {string} aArea 6711 * The area ID you want to move the widget to. This can also be 6712 * CustomizableUI.AREA_NO_AREA to see if the widget can move to the 6713 * customization palette, whether it's removable or not. 6714 * @returns {boolean} 6715 * True if this is possible. The same caveats as for isWidgetRemovable 6716 * apply, however, if no windows are open. 6717 */ 6718 canWidgetMoveToArea(aWidgetId, aArea) { 6719 return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea); 6720 }, 6721 /** 6722 * Whether we're in a default state. Note that non-removable non-default 6723 * widgets and non-existing widgets are not taken into account in determining 6724 * whether we're in the default state. 6725 * 6726 * NB: this is a property with a getter. The getter is NOT cheap, because 6727 * it does smart things with non-removable non-default items, non-existent 6728 * items, and so forth. Please don't call unless necessary. 6729 */ 6730 get inDefaultState() { 6731 return CustomizableUIInternal.inDefaultState; 6732 }, 6733 6734 /** 6735 * Set a toolbar's visibility state in all windows. 6736 * 6737 * @param {string} aToolbarId 6738 * The toolbar whose visibility should be adjusted. 6739 * @param {boolean} aIsVisible 6740 * Whether the toolbar should be made visible. 6741 */ 6742 setToolbarVisibility(aToolbarId, aIsVisible) { 6743 CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible); 6744 }, 6745 6746 /** 6747 * Returns a Set with the IDs of any registered toolbar areas that are 6748 * currently collapsed in a particular window. Menubars that are set to 6749 * autohide and are in the temporary "open" state are still considered 6750 * collapsed by default. 6751 * 6752 * @param {Window} window The browser window to check for collapsed toolbars. 6753 * @returns {Set<string>} 6754 */ 6755 getCollapsedToolbarIds(window) { 6756 return CustomizableUIInternal.getCollapsedToolbarIds(window); 6757 }, 6758 6759 /** 6760 * Checks if a widget is likely visible in a given window. 6761 * 6762 * This method returns true when a widget is: 6763 * - Not pinned to the overflow menu 6764 * - Not in a collapsed toolbar (e.g. bookmarks toolbar, menu bar) 6765 * - Not in the customization palette 6766 * 6767 * Note: A widget that is moved into the overflow menu due to 6768 * the window being small might be considered visible by 6769 * this method, because a widget's placement does not 6770 * change when it overflows into the overflow menu. 6771 * 6772 * @param {string} aWidgetId the widget ID to check. 6773 * @param {Window} window The browser window to check for widget visibility. 6774 * @returns {boolean} whether the given widget is likely visible or not. 6775 */ 6776 widgetIsLikelyVisible(aWidgetId, window) { 6777 return CustomizableUIInternal.widgetIsLikelyVisible(aWidgetId, window); 6778 }, 6779 6780 /** 6781 * DEPRECATED! Use fluent instead. 6782 * 6783 * Get a localized property off a (widget?) object. 6784 * 6785 * NB: this is unlikely to be useful unless you're in Firefox code, because 6786 * this code uses the builtin widget stringbundle, and can't be told 6787 * to use add-on-provided strings. It's mainly here as convenience for 6788 * custom builtin widgets that build their own DOM but use the same 6789 * stringbundle as the other builtin widgets. 6790 * 6791 * @param {string|object} aWidget 6792 * The ID of a widget, or a widget object whose properties we should use to 6793 * fetch a localizable string. 6794 * @param {string} aProp 6795 * The property on the object to use for the fetching from 6796 * customizableWidgets.properties. 6797 * @param {string[]} [aFormatArgs] 6798 * Any extra arguments to use for a formatted string. 6799 * @param {string} [aDef] 6800 * The default to return if we don't find the string in the stringbundle. 6801 * @returns {string} 6802 * The localized string, or aDef if the string isn't in the bundle. If no 6803 * default is provided, if aProp exists on aWidget, we'll return that, 6804 * otherwise we'll return the empty string. 6805 */ 6806 getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) { 6807 return CustomizableUIInternal.getLocalizedProperty( 6808 aWidget, 6809 aProp, 6810 aFormatArgs, 6811 aDef 6812 ); 6813 }, 6814 /** 6815 * Utility function to detect, find and set a keyboard shortcut for a menuitem 6816 * or (toolbar)button. 6817 * 6818 * @param {Element} aShortcutNode 6819 * The XUL node where the shortcut will be derived from; 6820 * @param {Element|null} aTargetNode 6821 * The XUL node on which the `shortcut` attribute will be set. If NULL, the 6822 * shortcut will be set on aShortcutNode. 6823 */ 6824 addShortcut(aShortcutNode, aTargetNode) { 6825 return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode); 6826 }, 6827 /** 6828 * Given a node, walk up to the first panel in its ancestor chain, and 6829 * close it. 6830 * 6831 * @param {Element} aNode a node whose panel should be closed. 6832 */ 6833 hidePanelForNode(aNode) { 6834 CustomizableUIInternal.hidePanelForNode(aNode); 6835 }, 6836 /** 6837 * Check if a widget is a "special" widget: a spring, spacer or separator. 6838 * 6839 * @param {string} aWidgetId the widget ID to check. 6840 * @returns {boolean} true if the widget is 'special', false otherwise. 6841 */ 6842 isSpecialWidget(aWidgetId) { 6843 return CustomizableUIInternal.isSpecialWidget(aWidgetId); 6844 }, 6845 /** 6846 * Check if a widget is provided by an extension. This effectively checks 6847 * whether `webExtension: true` passed when the widget was being created. 6848 * 6849 * If the widget being referred to hasn't yet been created, or has been 6850 * destroyed, we fallback to checking the ID for the "-browser-action" 6851 * suffix. 6852 * 6853 * @param {string} aWidgetId the widget ID to check. 6854 * @returns {boolean} 6855 * True if the widget was provided by an extension, false otherwise. 6856 */ 6857 isWebExtensionWidget(aWidgetId) { 6858 if (typeof aWidgetId !== "string") { 6859 return false; 6860 } 6861 let widget = CustomizableUI.getWidget(aWidgetId); 6862 return widget?.webExtension || aWidgetId.endsWith("-browser-action"); 6863 }, 6864 /** 6865 * Add listeners to a panel that will close it. For use from the menu panel 6866 * and overflowable toolbar implementations, unlikely to be useful for other 6867 * consumers. 6868 * 6869 * @param {Element} aPanel 6870 * The panel to which listeners should be attached. 6871 */ 6872 addPanelCloseListeners(aPanel) { 6873 CustomizableUIInternal.addPanelCloseListeners(aPanel); 6874 }, 6875 /** 6876 * Remove close listeners that have been added to a panel with 6877 * addPanelCloseListeners. For use from the menu panel and overflowable 6878 * toolbar implementations, unlikely to be useful for consumers. 6879 * 6880 * @param {Element} aPanel 6881 * The panel from which listeners should be removed. 6882 */ 6883 removePanelCloseListeners(aPanel) { 6884 CustomizableUIInternal.removePanelCloseListeners(aPanel); 6885 }, 6886 /** 6887 * Notify listeners a widget is about to be dragged to an area. For use from 6888 * Customize Mode only, do not use otherwise. 6889 * 6890 * @param {string} aWidgetId 6891 * The ID of the widget that is being dragged to an area. 6892 * @param {string} aArea 6893 * The ID of the area to which the widget is being dragged. 6894 */ 6895 onWidgetDrag(aWidgetId, aArea) { 6896 CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea); 6897 }, 6898 /** 6899 * Notify listeners that a window is entering customize mode. For use from 6900 * Customize Mode only, do not use otherwise. 6901 * 6902 * @param {DOMWindow} aWindow 6903 * The window entering customize mode. 6904 */ 6905 notifyStartCustomizing(aWindow) { 6906 CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow); 6907 }, 6908 /** 6909 * Notify listeners that a window is exiting customize mode. For use from 6910 * Customize Mode only, do not use otherwise. 6911 * 6912 * @param {DOMWindow} aWindow 6913 * The window exiting customize mode. 6914 */ 6915 notifyEndCustomizing(aWindow) { 6916 CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow); 6917 }, 6918 6919 /** 6920 * Notify toolbox(es) of a particular event. If you don't pass aWindow, 6921 * all toolboxes will be notified. For use from Customize Mode only, 6922 * do not use otherwise. 6923 * 6924 * @param {string} aEvent 6925 * The name of the event to send. 6926 * @param {object} [aDetails={}] 6927 * The details of the event. 6928 * @param {DOMWindow|null} [aWindow=null] 6929 * The window in which to send the event. 6930 */ 6931 dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) { 6932 CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow); 6933 }, 6934 6935 /** 6936 * Check whether an area is overflowable. 6937 * 6938 * @param {string} aAreaId 6939 * The ID of an area to check for overflowable-ness. 6940 * @returns {boolean} 6941 * True if the area is overflowable, false otherwise. 6942 */ 6943 isAreaOverflowable(aAreaId) { 6944 let area = gAreas.get(aAreaId); 6945 return area 6946 ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable") 6947 : false; 6948 }, 6949 /** 6950 * Obtain a string indicating the place of an element. This is intended 6951 * for use from customize mode; You should generally use getPlacementOfWidget 6952 * instead, which is cheaper because it does not use the DOM. 6953 * 6954 * @param {DOMElement} aElement 6955 * The DOM node whose place we need to check. 6956 * @returns {string|undefined} 6957 * "toolbar" if the node is in a toolbar, "panel" if it is in the menu 6958 * panel, "palette" if it is in the (visible!) customization palette, 6959 * undefined otherwise. 6960 */ 6961 getPlaceForItem(aElement) { 6962 let place; 6963 let node = aElement; 6964 while (node && !place) { 6965 if (node.localName == "toolbar") { 6966 place = "toolbar"; 6967 } else if (node.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { 6968 place = "panel"; 6969 } else if (node.id == "customization-palette") { 6970 place = "palette"; 6971 } 6972 6973 node = node.parentNode; 6974 } 6975 return place; 6976 }, 6977 6978 /** 6979 * Check if a toolbar is builtin or not. 6980 * 6981 * @param {string} aToolbarId 6982 * The ID of the toolbar you want to check. 6983 */ 6984 isBuiltinToolbar(aToolbarId) { 6985 return CustomizableUIInternal.builtinToolbars.has(aToolbarId); 6986 }, 6987 6988 /** 6989 * Create an instance of a spring, spacer or separator. 6990 * 6991 * @param {string} aId 6992 * The type of special widget (spring, spacer or separator). 6993 * @param {Document} aDocument 6994 * The document in which to create it. 6995 * @returns {Element} 6996 * The created spring, spacer or separator node. 6997 */ 6998 createSpecialWidget(aId, aDocument) { 6999 return CustomizableUIInternal.createSpecialWidget(aId, aDocument); 7000 }, 7001 7002 /** 7003 * Fills a submenu with menu items. 7004 * 7005 * @param {Element[]} aMenuItems 7006 * The array of menu items to display. 7007 * @param {Element} aSubview 7008 * The subview to fill with the menu items. 7009 */ 7010 fillSubviewFromMenuItems(aMenuItems, aSubview) { 7011 let attrs = [ 7012 "oncommand", 7013 "onclick", 7014 "label", 7015 "key", 7016 "disabled", 7017 "command", 7018 "observes", 7019 "hidden", 7020 "class", 7021 "origin", 7022 "image", 7023 "checked", 7024 "style", 7025 ]; 7026 7027 // Use ownerGlobal.document to ensure we get the right doc even for 7028 // elements in template tags. 7029 let doc = aSubview.ownerGlobal.document; 7030 let fragment = doc.createDocumentFragment(); 7031 for (let menuChild of aMenuItems) { 7032 if (menuChild.hidden) { 7033 continue; 7034 } 7035 7036 let subviewItem; 7037 if (menuChild.localName == "menuseparator") { 7038 // Don't insert duplicate or leading separators. This can happen if there are 7039 // menus (which we don't copy) above the separator. 7040 if ( 7041 !fragment.lastElementChild || 7042 fragment.lastElementChild.localName == "toolbarseparator" 7043 ) { 7044 continue; 7045 } 7046 subviewItem = doc.createXULElement("toolbarseparator"); 7047 } else if (menuChild.localName == "menuitem") { 7048 subviewItem = doc.createXULElement("toolbarbutton"); 7049 CustomizableUI.addShortcut(menuChild, subviewItem); 7050 7051 let item = menuChild; 7052 if (!item.hasAttribute("onclick")) { 7053 subviewItem.addEventListener("click", event => { 7054 let newEvent = new doc.ownerGlobal.PointerEvent("click", event); 7055 7056 // Telemetry should only pay attention to the original event. 7057 lazy.BrowserUsageTelemetry.ignoreEvent(newEvent); 7058 item.dispatchEvent(newEvent); 7059 }); 7060 } 7061 7062 if (!item.hasAttribute("oncommand")) { 7063 subviewItem.addEventListener("command", event => { 7064 let newEvent = doc.createEvent("XULCommandEvent"); 7065 newEvent.initCommandEvent( 7066 event.type, 7067 event.bubbles, 7068 event.cancelable, 7069 event.view, 7070 event.detail, 7071 event.ctrlKey, 7072 event.altKey, 7073 event.shiftKey, 7074 event.metaKey, 7075 0, 7076 event.sourceEvent, 7077 0 7078 ); 7079 7080 // Telemetry should only pay attention to the original event. 7081 lazy.BrowserUsageTelemetry.ignoreEvent(newEvent); 7082 item.dispatchEvent(newEvent); 7083 }); 7084 } 7085 } else { 7086 continue; 7087 } 7088 for (let attr of attrs) { 7089 let attrVal = menuChild.getAttribute(attr); 7090 if (attrVal) { 7091 subviewItem.setAttribute(attr, attrVal); 7092 } 7093 } 7094 // We do this after so the .subviewbutton class doesn't get overriden. 7095 if (menuChild.localName == "menuitem") { 7096 subviewItem.classList.add("subviewbutton"); 7097 } 7098 7099 // We make it possible to supply an alternative Fluent key when cloning 7100 // this menuitem into the AppMenu or panel contexts. This is because 7101 // we often use Title Case in menuitems in native menus, but want to use 7102 // Sentence case in the AppMenu / panels. 7103 let l10nId = menuChild.getAttribute("appmenu-data-l10n-id"); 7104 if (l10nId) { 7105 doc.l10n.setAttributes(subviewItem, l10nId); 7106 } 7107 7108 fragment.appendChild(subviewItem); 7109 } 7110 aSubview.appendChild(fragment); 7111 }, 7112 7113 /** 7114 * A helper function for clearing subviews. 7115 * 7116 * @param {Element} aSubview 7117 * The subview to clear. 7118 */ 7119 clearSubview(aSubview) { 7120 let parent = aSubview.parentNode; 7121 // We'll take the container out of the document before cleaning it out 7122 // to avoid reflowing each time we remove something. 7123 parent.removeChild(aSubview); 7124 7125 while (aSubview.firstChild) { 7126 aSubview.firstChild.remove(); 7127 } 7128 7129 parent.appendChild(aSubview); 7130 }, 7131 7132 /** 7133 * Called when DOMContentLoaded fires for a new browser window. 7134 * 7135 * @param {DOMWindow} aWindow 7136 * The DOM Window that has just opened. 7137 */ 7138 handleNewBrowserWindow(aWindow) { 7139 return CustomizableUIInternal.handleNewBrowserWindow(aWindow); 7140 }, 7141 7142 /** 7143 * Given a DOM node with the `customizable` attribute, will attempt to resolve 7144 * it to the associated "customization target" for that DOM node via its 7145 * `customizationtarget` attribute. If no such attribute exists, the DOM node 7146 * itself is returned. 7147 * 7148 * If the DOM node is null, is not customizable, or cannot be resolved to 7149 * a customization target, then null is returned. 7150 * 7151 * @param {Element|null} aElement 7152 * The DOM node to resolve to a customization target DOM node. 7153 * @returns {Element|null} 7154 * The customization target DOM node, or null if one cannot be found. 7155 */ 7156 getCustomizationTarget(aElement) { 7157 return CustomizableUIInternal.getCustomizationTarget(aElement); 7158 }, 7159 7160 /** 7161 * This is a test-only method that allows tests to violate encapsulation and 7162 * gain access to some state internal to this module. If not running in test 7163 * automation, this will always return null. 7164 * 7165 * @param {string} aProp 7166 * The string representation of the internal property to retrieve. Only some 7167 * properties are supported - see the method code. 7168 * @returns {any|null} 7169 */ 7170 getTestOnlyInternalProp(aProp) { 7171 if (!Cu.isInAutomation) { 7172 return null; 7173 } 7174 switch (aProp) { 7175 case "CustomizableUIInternal": 7176 return CustomizableUIInternal; 7177 case "gAreas": 7178 return gAreas; 7179 case "gFuturePlacements": 7180 return gFuturePlacements; 7181 case "gPalette": 7182 return gPalette; 7183 case "gPlacements": 7184 return gPlacements; 7185 case "gSavedState": 7186 return gSavedState; 7187 case "gSeenWidgets": 7188 return gSeenWidgets; 7189 case "kVersion": 7190 return kVersion; 7191 } 7192 return null; 7193 }, 7194 7195 /** 7196 * This is a test-only method that allows tests to violate encapsulation and 7197 * write to some state internal to this module. If not running in test 7198 * automation, this will always just immediately return without making any 7199 * changes. 7200 * 7201 * @param {string} aProp 7202 * The string representation of the internal property to change. Only some 7203 * properties are supported - see the method code. 7204 * @param {any} aValue 7205 * The value to set the property to. 7206 */ 7207 setTestOnlyInternalProp(aProp, aValue) { 7208 if (!Cu.isInAutomation) { 7209 return; 7210 } 7211 switch (aProp) { 7212 case "gSavedState": 7213 gSavedState = aValue; 7214 break; 7215 case "kVersion": 7216 kVersion = aValue; 7217 break; 7218 case "gDirty": 7219 gDirty = aValue; 7220 break; 7221 } 7222 }, 7223 }; 7224 7225 Object.freeze(CustomizableUI); 7226 Object.freeze(CustomizableUI.windows); 7227 7228 /** 7229 * All external consumers of widgets are really interacting with these wrappers 7230 * which provide a common interface. 7231 */ 7232 7233 /** 7234 * WidgetGroupWrapper is the common interface for interacting with an entire 7235 * widget group - AKA, all instances of a widget across a series of windows. 7236 * This particular wrapper is only used for widgets created via the provider 7237 * API. 7238 */ 7239 function WidgetGroupWrapper(aWidget) { 7240 this.isGroup = true; 7241 7242 const kBareProps = [ 7243 "id", 7244 "source", 7245 "type", 7246 "disabled", 7247 "label", 7248 "tooltiptext", 7249 "showInPrivateBrowsing", 7250 "hideInNonPrivateBrowsing", 7251 "viewId", 7252 "disallowSubView", 7253 "webExtension", 7254 ]; 7255 for (let prop of kBareProps) { 7256 let propertyName = prop; 7257 this.__defineGetter__(propertyName, () => aWidget[propertyName]); 7258 } 7259 7260 this.__defineGetter__("provider", () => CustomizableUI.PROVIDER_API); 7261 7262 this.__defineSetter__("disabled", function (aValue) { 7263 aValue = !!aValue; 7264 aWidget.disabled = aValue; 7265 for (let [, instance] of aWidget.instances) { 7266 instance.disabled = aValue; 7267 } 7268 }); 7269 7270 this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) { 7271 let wrapperMap; 7272 if (!gSingleWrapperCache.has(aWindow)) { 7273 wrapperMap = new Map(); 7274 gSingleWrapperCache.set(aWindow, wrapperMap); 7275 } else { 7276 wrapperMap = gSingleWrapperCache.get(aWindow); 7277 } 7278 if (wrapperMap.has(aWidget.id)) { 7279 return wrapperMap.get(aWidget.id); 7280 } 7281 7282 let instance = aWidget.instances.get(aWindow.document); 7283 if (!instance) { 7284 instance = CustomizableUIInternal.buildWidgetNode( 7285 aWindow.document, 7286 aWidget 7287 ); 7288 } 7289 7290 let wrapper = new WidgetSingleWrapper(aWidget, instance); 7291 wrapperMap.set(aWidget.id, wrapper); 7292 return wrapper; 7293 }; 7294 7295 this.__defineGetter__("instances", function () { 7296 // Can't use gBuildWindows here because some areas load lazily: 7297 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); 7298 if (!placement) { 7299 return []; 7300 } 7301 let area = placement.area; 7302 let buildAreas = gBuildAreas.get(area); 7303 if (!buildAreas) { 7304 return []; 7305 } 7306 return Array.from(buildAreas, node => this.forWindow(node.ownerGlobal)); 7307 }); 7308 7309 this.__defineGetter__("areaType", function () { 7310 let areaProps = gAreas.get(aWidget.currentArea); 7311 return areaProps && areaProps.get("type"); 7312 }); 7313 7314 Object.freeze(this); 7315 } 7316 7317 /** 7318 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in 7319 * a particular window. 7320 */ 7321 function WidgetSingleWrapper(aWidget, aNode) { 7322 this.isGroup = false; 7323 7324 this.node = aNode; 7325 this.provider = CustomizableUI.PROVIDER_API; 7326 7327 const kGlobalProps = ["id", "type"]; 7328 for (let prop of kGlobalProps) { 7329 this[prop] = aWidget[prop]; 7330 } 7331 7332 const kNodeProps = ["label", "tooltiptext"]; 7333 for (let prop of kNodeProps) { 7334 let propertyName = prop; 7335 // Look at the node for these, instead of the widget data, to ensure the 7336 // wrapper always reflects this live instance. 7337 this.__defineGetter__(propertyName, () => aNode.getAttribute(propertyName)); 7338 } 7339 7340 this.__defineGetter__("disabled", () => aNode.disabled); 7341 this.__defineSetter__("disabled", function (aValue) { 7342 aNode.disabled = !!aValue; 7343 }); 7344 7345 this.__defineGetter__("anchor", function () { 7346 let anchorId; 7347 // First check for an anchor for the area: 7348 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); 7349 if (placement) { 7350 anchorId = gAreas.get(placement.area).get("anchor"); 7351 } 7352 if (!anchorId) { 7353 anchorId = aNode.getAttribute("cui-anchorid"); 7354 } 7355 if (!anchorId) { 7356 anchorId = aNode.getAttribute("view-button-id"); 7357 } 7358 if (anchorId) { 7359 return aNode.ownerDocument.getElementById(anchorId); 7360 } 7361 if (aWidget.type == "button-and-view") { 7362 return aNode.lastElementChild; 7363 } 7364 return aNode; 7365 }); 7366 7367 this.__defineGetter__("overflowed", function () { 7368 return aNode.getAttribute("overflowedItem") == "true"; 7369 }); 7370 7371 Object.freeze(this); 7372 } 7373 7374 /** 7375 * XULWidgetGroupWrapper is the common interface for interacting with an entire 7376 * widget group - AKA, all instances of a widget across a series of windows. 7377 * This particular wrapper is only used for widgets created via the old-school 7378 * XUL method (overlays, or programmatically injecting toolbaritems, or other 7379 * such things). 7380 */ 7381 // XXXunf Going to need to hook this up to some events to keep it all live. 7382 function XULWidgetGroupWrapper(aWidgetId) { 7383 this.isGroup = true; 7384 this.id = aWidgetId; 7385 this.type = "custom"; 7386 // XUL Widgets can never be provided by extensions. 7387 this.webExtension = false; 7388 this.provider = CustomizableUI.PROVIDER_XUL; 7389 7390 this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) { 7391 let wrapperMap; 7392 if (!gSingleWrapperCache.has(aWindow)) { 7393 wrapperMap = new Map(); 7394 gSingleWrapperCache.set(aWindow, wrapperMap); 7395 } else { 7396 wrapperMap = gSingleWrapperCache.get(aWindow); 7397 } 7398 if (wrapperMap.has(aWidgetId)) { 7399 return wrapperMap.get(aWidgetId); 7400 } 7401 7402 let instance = aWindow.document.getElementById(aWidgetId); 7403 if (!instance) { 7404 // Toolbar palettes aren't part of the document, so elements in there 7405 // won't be found via document.getElementById(). 7406 instance = aWindow.gNavToolbox.palette.getElementsByAttribute( 7407 "id", 7408 aWidgetId 7409 )[0]; 7410 } 7411 7412 let wrapper = new XULWidgetSingleWrapper( 7413 aWidgetId, 7414 instance, 7415 aWindow.document 7416 ); 7417 wrapperMap.set(aWidgetId, wrapper); 7418 return wrapper; 7419 }; 7420 7421 this.__defineGetter__("areaType", function () { 7422 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); 7423 if (!placement) { 7424 return null; 7425 } 7426 7427 let areaProps = gAreas.get(placement.area); 7428 return areaProps && areaProps.get("type"); 7429 }); 7430 7431 this.__defineGetter__("instances", function () { 7432 return Array.from(gBuildWindows, wins => this.forWindow(wins[0])); 7433 }); 7434 7435 Object.freeze(this); 7436 } 7437 7438 /** 7439 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL 7440 * widget in a particular window. 7441 */ 7442 function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) { 7443 this.isGroup = false; 7444 7445 this.id = aWidgetId; 7446 this.type = "custom"; 7447 this.provider = CustomizableUI.PROVIDER_XUL; 7448 7449 let weakDoc = Cu.getWeakReference(aDocument); 7450 // If we keep a strong ref, the weak ref will never die, so null it out: 7451 aDocument = null; 7452 7453 this.__defineGetter__("node", function () { 7454 // If we've set this to null (further down), we're sure there's nothing to 7455 // be gotten here, so bail out early: 7456 if (!weakDoc) { 7457 return null; 7458 } 7459 if (aNode) { 7460 // Return the last known node if it's still in the DOM... 7461 if (aNode.isConnected) { 7462 return aNode; 7463 } 7464 // ... or the toolbox 7465 let toolbox = aNode.ownerGlobal.gNavToolbox; 7466 if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) { 7467 return aNode; 7468 } 7469 // If it isn't, clear the cached value and fall through to the "slow" case: 7470 aNode = null; 7471 } 7472 7473 let doc = weakDoc.get(); 7474 if (doc) { 7475 // Store locally so we can cache the result: 7476 aNode = CustomizableUIInternal.findXULWidgetInWindow( 7477 aWidgetId, 7478 doc.defaultView 7479 ); 7480 return aNode; 7481 } 7482 // The weakref to the document is dead, we're done here forever more: 7483 weakDoc = null; 7484 return null; 7485 }); 7486 7487 this.__defineGetter__("anchor", function () { 7488 let anchorId; 7489 // First check for an anchor for the area: 7490 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); 7491 if (placement) { 7492 anchorId = gAreas.get(placement.area).get("anchor"); 7493 } 7494 7495 let node = this.node; 7496 if (!anchorId && node) { 7497 anchorId = node.getAttribute("cui-anchorid"); 7498 } 7499 7500 return anchorId && node 7501 ? node.ownerDocument.getElementById(anchorId) 7502 : node; 7503 }); 7504 7505 this.__defineGetter__("overflowed", function () { 7506 let node = this.node; 7507 if (!node) { 7508 return false; 7509 } 7510 return node.getAttribute("overflowedItem") == "true"; 7511 }); 7512 7513 Object.freeze(this); 7514 } 7515 7516 /** 7517 * OverflowableToolbar is a class that gives a <xul:toolbar> the ability to send 7518 * toolbar items that are "overflowable" to lists in separate panels if and 7519 * when the toolbar shrinks enough so that those items overflow out of bounds. 7520 * Secondly, this class manages moving things out from those panels and back 7521 * into the toolbar once it underflows and has the space to accommodate the 7522 * items that had originally overflowed out. 7523 * 7524 * There are two panels that toolbar items can be overflowed to: 7525 * 7526 * 1. The default items overflow panel 7527 * This is where built-in default toolbar items will go to. 7528 * 2. The Unified Extensions panel 7529 * This is where browser_action toolbar buttons created by extensions will 7530 * go to if the Unified Extensions UI is enabled - otherwise, those items will 7531 * go to the default items overflow panel. 7532 * 7533 * Finally, OverflowableToolbar manages the showing of the default items 7534 * overflow panel when the associated anchor is clicked or dragged over. The 7535 * Unified Extensions panel is managed separately by the extension code. 7536 * 7537 * In theory, we could have multiple overflowable toolbars, but in practice, 7538 * only the nav-bar (CustomizableUI.AREA_NAVBAR) makes use of this class. 7539 */ 7540 class OverflowableToolbar { 7541 /** 7542 * The OverflowableToolbar class is constructed during browser window 7543 * creation, but to optimize for window painting, we defer most work until 7544 * after the window has painted. This property is set to true once 7545 * initialization has completed. 7546 * 7547 * @type {boolean} 7548 */ 7549 #initialized = false; 7550 7551 /** 7552 * A reference to the <xul:toolbar> that is overflowable. 7553 * 7554 * @type {Element} 7555 */ 7556 #toolbar = null; 7557 7558 /** 7559 * A reference to the part of the <xul:toolbar> that accepts CustomizableUI 7560 * widgets. 7561 * 7562 * @type {Element} 7563 */ 7564 #target = null; 7565 7566 /** 7567 * A mapping from the ID of a toolbar item that has overflowed to the width 7568 * that the toolbar item occupied in the toolbar at the time of overflow. Any 7569 * item that is currently overflowed will have an entry in this map. 7570 * 7571 * @type {Map<string, number>} 7572 */ 7573 #overflowedInfo = new Map(); 7574 7575 /** 7576 * The set of overflowed DOM nodes that were hidden at the time of overflowing. 7577 */ 7578 #hiddenOverflowedNodes = new WeakSet(); 7579 7580 /** 7581 * True if the overflowable toolbar is actively handling overflows and 7582 * underflows. This value is set internally by the private #enable() and 7583 * #disable() methods. 7584 * 7585 * @type {boolean} 7586 */ 7587 #enabled = true; 7588 7589 /** 7590 * A reference to the element that overflowed toolbar items will be 7591 * appended to as children upon overflow. 7592 * 7593 * @type {Element} 7594 */ 7595 #defaultList = null; 7596 7597 /** 7598 * A reference to the button that opens the overflow panel. This is also 7599 * the element that the panel will anchor to. 7600 * 7601 * @type {Element} 7602 */ 7603 #defaultListButton = null; 7604 7605 /** 7606 * A reference to the <xul:panel> overflow panel that contains the #defaultList 7607 * element. 7608 * 7609 * @type {Element} 7610 */ 7611 #defaultListPanel = null; 7612 7613 /** 7614 * A reference to the the element that overflowed extension browser action 7615 * toolbar items will be appended to as children upon overflow if the 7616 * Unified Extension UI is enabled. This is created lazily and might be null, 7617 * so you should use the #webExtList memoizing getter instead to get this. 7618 * 7619 * @type {Element|null} 7620 */ 7621 #webExtListRef = null; 7622 7623 /** 7624 * An empty object that is created in #checkOverflow to identify individual 7625 * calls to #checkOverflow and avoid re-entrancy (since #checkOverflow is 7626 * asynchronous, and in theory, could be called multiple times before any of 7627 * those times have a chance to fully exit). 7628 * 7629 * @type {object} 7630 */ 7631 #checkOverflowHandle = null; 7632 7633 /** 7634 * A timeout ID returned by setTimeout that identifies a timeout function that 7635 * runs to hide the #defaultListPanel if the user happened to open the panel by dragging 7636 * over the #defaultListButton and then didn't hover any part of the #defaultListPanel. 7637 * 7638 * @type {number} 7639 */ 7640 #hideTimeoutId = null; 7641 7642 /** 7643 * Public methods start here. 7644 */ 7645 7646 /** 7647 * OverflowableToolbar constructor. This is run very early on in the lifecycle 7648 * of a browser window, so it tries to defer most work to the init() method 7649 * instead after first paint. 7650 * 7651 * Upon construction, a "overflowable" attribute will be set on the 7652 * toolbar, set to the value of "true". 7653 * 7654 * Part of the API for OverflowableToolbar is declarative, in that it expects 7655 * certain attributes to be set on the <xul:toolbar> that is overflowable. 7656 * Those attributes are: 7657 * 7658 * default-overflowbutton: 7659 * The ID of the button that is used to open and anchor the overflow panel. 7660 * default-overflowtarget: 7661 * The ID of the element that overflowed items will be appended to as 7662 * children. Note that the overflowed toolbar items are moved into and out 7663 * of this overflow target, so it is definitely advisable to let 7664 * OverflowableToolbar own managing the children of default-overflowtarget, 7665 * and to not modify it outside of this class. 7666 * default-overflowpanel: 7667 * The ID of the <xul:panel> that contains the default-overflowtarget. 7668 * addon-webext-overflowbutton: 7669 * The ID of the button that is used to open and anchor the Unified 7670 * Extensions panel. 7671 * addon-webext-overflowtarget: 7672 * The ID of the element that overflowed extension toolbar buttons will 7673 * be appended to as children if the Unified Extensions UI is enabled. 7674 * Note that the overflowed toolbar items are moved into and out of this 7675 * overflow target, so it is definitely advisable to let OverflowableToolbar 7676 * own managing the children of addon-webext-overflowtarget, and to not 7677 * modify it outside of this class. 7678 * 7679 * @param {Element} aToolbarNode The <xul:toolbar> that will be overflowable. 7680 * @throws {Error} Throws if the customization target of the toolbar somehow 7681 * isn't a direct descendent of the toolbar. 7682 */ 7683 constructor(aToolbarNode) { 7684 this.#toolbar = aToolbarNode; 7685 this.#target = CustomizableUI.getCustomizationTarget(this.#toolbar); 7686 if (this.#target.parentNode != this.#toolbar) { 7687 throw new Error( 7688 "Customization target must be a direct child of an overflowable toolbar." 7689 ); 7690 } 7691 7692 this.#toolbar.setAttribute("overflowable", "true"); 7693 let doc = this.#toolbar.ownerDocument; 7694 this.#defaultList = doc.getElementById( 7695 this.#toolbar.getAttribute("default-overflowtarget") 7696 ); 7697 this.#defaultList._customizationTarget = this.#defaultList; 7698 7699 let window = this.#toolbar.ownerGlobal; 7700 7701 if (window.gBrowserInit.delayedStartupFinished) { 7702 this.init(); 7703 } else { 7704 Services.obs.addObserver(this, "browser-delayed-startup-finished"); 7705 } 7706 } 7707 7708 /** 7709 * Does final initialization of the OverflowableToolbar after the window has 7710 * first painted. This will also kick off the first check to see if overflow 7711 * has already occurred at the time of initialization. 7712 */ 7713 init() { 7714 let doc = this.#toolbar.ownerDocument; 7715 let window = doc.defaultView; 7716 window.addEventListener("resize", this); 7717 window.gNavToolbox.addEventListener("customizationstarting", this); 7718 window.gNavToolbox.addEventListener("aftercustomization", this); 7719 7720 let defaultListButton = this.#toolbar.getAttribute( 7721 "default-overflowbutton" 7722 ); 7723 this.#defaultListButton = doc.getElementById(defaultListButton); 7724 this.#defaultListButton.addEventListener("mousedown", this); 7725 this.#defaultListButton.addEventListener("keypress", this); 7726 this.#defaultListButton.addEventListener("dragover", this); 7727 this.#defaultListButton.addEventListener("dragend", this); 7728 7729 let panelId = this.#toolbar.getAttribute("default-overflowpanel"); 7730 this.#defaultListPanel = doc.getElementById(panelId); 7731 this.#defaultListPanel.addEventListener("popuphiding", this); 7732 CustomizableUIInternal.addPanelCloseListeners(this.#defaultListPanel); 7733 7734 CustomizableUI.addListener(this); 7735 7736 this.#checkOverflow(); 7737 7738 this.#initialized = true; 7739 } 7740 7741 /** 7742 * Almost the exact reverse of init(). This is called when the browser window 7743 * is unloading. 7744 */ 7745 uninit() { 7746 this.#toolbar.removeAttribute("overflowable"); 7747 7748 if (!this.#initialized) { 7749 Services.obs.removeObserver(this, "browser-delayed-startup-finished"); 7750 Services.prefs.removeObserver(kPrefSidebarVerticalTabsEnabled, this); 7751 Services.prefs.removeObserver(kPrefSidebarRevampEnabled, this); 7752 return; 7753 } 7754 7755 this.#disable(); 7756 7757 let window = this.#toolbar.ownerGlobal; 7758 window.removeEventListener("resize", this); 7759 window.gNavToolbox.removeEventListener("customizationstarting", this); 7760 window.gNavToolbox.removeEventListener("aftercustomization", this); 7761 this.#defaultListButton.removeEventListener("mousedown", this); 7762 this.#defaultListButton.removeEventListener("keypress", this); 7763 this.#defaultListButton.removeEventListener("dragover", this); 7764 this.#defaultListButton.removeEventListener("dragend", this); 7765 this.#defaultListPanel.removeEventListener("popuphiding", this); 7766 7767 CustomizableUI.removeListener(this); 7768 CustomizableUIInternal.removePanelCloseListeners(this.#defaultListPanel); 7769 } 7770 7771 /** 7772 * Opens the overflow #defaultListPanel if it's not already open. If the panel is in 7773 * the midst of hiding when this is called, the panel will be re-opened. 7774 * 7775 * @returns {Promise<undefined>} 7776 * Resolves once the panel is open. 7777 */ 7778 show(aEvent) { 7779 if (this.#defaultListPanel.state == "open") { 7780 return Promise.resolve(); 7781 } 7782 return new Promise(resolve => { 7783 let doc = this.#defaultListPanel.ownerDocument; 7784 this.#defaultListPanel.hidden = false; 7785 let multiview = this.#defaultListPanel.querySelector("panelmultiview"); 7786 let mainViewId = multiview.getAttribute("mainViewId"); 7787 let mainView = doc.getElementById(mainViewId); 7788 let contextMenu = doc.getElementById(mainView.getAttribute("context")); 7789 contextMenu.addEventListener("command", this, { 7790 capture: true, 7791 mozSystemGroup: true, 7792 }); 7793 let anchor = this.#defaultListButton.icon; 7794 7795 let popupshown = false; 7796 this.#defaultListPanel.addEventListener( 7797 "popupshown", 7798 () => { 7799 popupshown = true; 7800 this.#defaultListPanel.addEventListener("dragover", this); 7801 this.#defaultListPanel.addEventListener("dragend", this); 7802 // Wait until the next tick to resolve so all popupshown 7803 // handlers have a chance to run before our promise resolution 7804 // handlers do. 7805 Services.tm.dispatchToMainThread(resolve); 7806 }, 7807 { once: true } 7808 ); 7809 7810 let openPanel = () => { 7811 // Ensure we update the gEditUIVisible flag when opening the popup, in 7812 // case the edit controls are in it. 7813 this.#defaultListPanel.addEventListener( 7814 "popupshowing", 7815 () => { 7816 doc.defaultView.updateEditUIVisibility(); 7817 }, 7818 { once: true } 7819 ); 7820 7821 this.#defaultListPanel.addEventListener( 7822 "popuphidden", 7823 () => { 7824 if (!popupshown) { 7825 // The panel was hidden again before it was shown. This can break 7826 // consumers waiting for the panel to show. So we try again. 7827 openPanel(); 7828 } 7829 }, 7830 { once: true } 7831 ); 7832 7833 lazy.PanelMultiView.openPopup( 7834 this.#defaultListPanel, 7835 anchor || this.#defaultListButton, 7836 { 7837 triggerEvent: aEvent, 7838 } 7839 ); 7840 this.#defaultListButton.open = true; 7841 }; 7842 7843 openPanel(); 7844 }); 7845 } 7846 7847 /** 7848 * Exposes whether #checkOverflow is currently running. 7849 * 7850 * @returns {boolean} True if #checkOverflow is currently running. 7851 */ 7852 isHandlingOverflow() { 7853 return !!this.#checkOverflowHandle; 7854 } 7855 7856 /** 7857 * Finds the most appropriate place to insert toolbar item aNode if we've been 7858 * asked to put it into the overflowable toolbar without being told exactly 7859 * where. 7860 * 7861 * @param {Element} aNode The toolbar item being inserted. 7862 * @returns {Array} [parent, nextNode] 7863 * parent: {Element} The parent element that should contain aNode. 7864 * nextNode: {Element|null} The node that should follow aNode after 7865 * insertion, if any. If this is null, aNode should be placed at the end 7866 * of parent. 7867 */ 7868 findOverflowedInsertionPoints(aNode) { 7869 let newNodeCanOverflow = aNode.getAttribute("overflows") != "false"; 7870 let areaId = this.#toolbar.id; 7871 let placements = gPlacements.get(areaId); 7872 let nodeIndex = placements.indexOf(aNode.id); 7873 let nodeBeforeNewNodeIsOverflown = false; 7874 7875 let loopIndex = -1; 7876 // Loop through placements to find where to insert this item. 7877 // As soon as we find an overflown widget, we will only 7878 // insert in the overflow panel (this is why we check placements 7879 // before the desired location for the new node). Once we pass 7880 // the desired location of the widget, we look for placement ids 7881 // that actually have DOM equivalents to insert before. If all 7882 // else fails, we insert at the end of either the overflow list 7883 // or the toolbar target. 7884 while (++loopIndex < placements.length) { 7885 let nextNodeId = placements[loopIndex]; 7886 if (loopIndex > nodeIndex) { 7887 // Note that if aNode is in a template, its `ownerDocument` is *not* 7888 // going to be the browser.xhtml document, so we cannot rely on it. 7889 let nextNode = this.#toolbar.ownerDocument.getElementById(nextNodeId); 7890 // If the node we're inserting can overflow, and the next node 7891 // in the toolbar is overflown, we should insert this node 7892 // in the overflow panel before it. 7893 if ( 7894 newNodeCanOverflow && 7895 this.#overflowedInfo.has(nextNodeId) && 7896 nextNode && 7897 nextNode.parentNode == this.#defaultList 7898 ) { 7899 return [this.#defaultList, nextNode]; 7900 } 7901 // Otherwise (if either we can't overflow, or the previous node 7902 // wasn't overflown), and the next node is in the toolbar itself, 7903 // insert the node in the toolbar. 7904 if ( 7905 (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) && 7906 nextNode && 7907 (nextNode.parentNode == this.#target || 7908 // Also check if the next node is in a customization wrapper 7909 // (toolbarpaletteitem). We don't need to do this for the 7910 // overflow case because overflow is disabled in customize mode. 7911 (nextNode.parentNode.localName == "toolbarpaletteitem" && 7912 nextNode.parentNode.parentNode == this.#target)) 7913 ) { 7914 return [this.#target, nextNode]; 7915 } 7916 } else if ( 7917 loopIndex < nodeIndex && 7918 this.#overflowedInfo.has(nextNodeId) 7919 ) { 7920 nodeBeforeNewNodeIsOverflown = true; 7921 } 7922 } 7923 7924 let overflowList = CustomizableUI.isWebExtensionWidget(aNode.id) 7925 ? this.#webExtList 7926 : this.#defaultList; 7927 7928 let containerForAppending = 7929 this.#overflowedInfo.size && newNodeCanOverflow 7930 ? overflowList 7931 : this.#target; 7932 return [containerForAppending, null]; 7933 } 7934 7935 /** 7936 * Allows callers to query for the current parent of a toolbar item that may 7937 * or may not be overflowed. That parent will either be #defaultList, 7938 * #webExtList (if it's an extension button) or #target. 7939 * 7940 * Note: It is assumed that the caller has verified that aNode is placed 7941 * within the toolbar customizable area according to CustomizableUI. 7942 * 7943 * @param {Element} aNode the node that can be overflowed by this 7944 * OverflowableToolbar. 7945 * @returns {Element} The current containing node for aNode. 7946 */ 7947 getContainerFor(aNode) { 7948 if (aNode.getAttribute("overflowedItem") == "true") { 7949 return CustomizableUI.isWebExtensionWidget(aNode.id) 7950 ? this.#webExtList 7951 : this.#defaultList; 7952 } 7953 return this.#target; 7954 } 7955 7956 /** 7957 * Private methods start here. 7958 */ 7959 7960 /** 7961 * Handle overflow in the toolbar by moving items to the overflow menu. 7962 */ 7963 async #onOverflow() { 7964 if (!this.#enabled) { 7965 return; 7966 } 7967 7968 let win = this.#target.ownerGlobal; 7969 let checkOverflowHandle = this.#checkOverflowHandle; 7970 let webExtButtonID = this.#toolbar.getAttribute( 7971 "addon-webext-overflowbutton" 7972 ); 7973 7974 let { isOverflowing, targetContentWidth } = await this.#getOverflowInfo(); 7975 7976 // Stop if the window has closed or if we re-enter while waiting for 7977 // layout. 7978 if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) { 7979 lazy.log.debug("Window closed or another overflow handler started."); 7980 return; 7981 } 7982 7983 let webExtList = this.#webExtList; 7984 7985 let child = this.#target.lastElementChild; 7986 while (child && isOverflowing) { 7987 let prevChild = child.previousElementSibling; 7988 7989 if (child.getAttribute("overflows") != "false") { 7990 this.#overflowedInfo.set(child.id, targetContentWidth); 7991 let { width: childWidth } = 7992 win.windowUtils.getBoundsWithoutFlushing(child); 7993 if (!childWidth) { 7994 this.#hiddenOverflowedNodes.add(child); 7995 } 7996 7997 child.setAttribute("overflowedItem", true); 7998 CustomizableUIInternal.ensureButtonContextMenu( 7999 child, 8000 this.#toolbar, 8001 true 8002 ); 8003 CustomizableUIInternal.notifyListeners( 8004 "onWidgetOverflow", 8005 child, 8006 this.#target 8007 ); 8008 8009 if (webExtList && CustomizableUI.isWebExtensionWidget(child.id)) { 8010 child.setAttribute("cui-anchorid", webExtButtonID); 8011 webExtList.insertBefore(child, webExtList.firstElementChild); 8012 } else { 8013 child.setAttribute("cui-anchorid", this.#defaultListButton.id); 8014 this.#defaultList.insertBefore( 8015 child, 8016 this.#defaultList.firstElementChild 8017 ); 8018 if (!CustomizableUI.isSpecialWidget(child.id) && childWidth) { 8019 this.#toolbar.setAttribute("overflowing", "true"); 8020 } 8021 } 8022 } 8023 child = prevChild; 8024 ({ isOverflowing, targetContentWidth } = await this.#getOverflowInfo()); 8025 // Stop if the window has closed or if we re-enter while waiting for 8026 // layout. 8027 if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) { 8028 lazy.log.debug("Window closed or another overflow handler started."); 8029 return; 8030 } 8031 } 8032 8033 win.UpdateUrlbarSearchSplitterState(); 8034 } 8035 8036 /** 8037 * @typedef {object} CustomizableUIOverflowInfo 8038 * @property {boolean} isOverflowing 8039 * True if at least one toolbar item has overflowed into an overflow panel. 8040 * @property {number} targetContentWidth 8041 * The total width of the items within the customization target area of the 8042 * overflowable toolbar toolbar. 8043 * @property {number} totalAvailWidth 8044 * The maximum width items in the overflowable toolbar may occupy before 8045 * causing an overflow. 8046 */ 8047 8048 /** 8049 * Returns a Promise that resolves to a an object that describes the state 8050 * that this OverflowableToolbar is currently in. 8051 * 8052 * @returns {Promise<CustomizableUIOverflowInfo>} 8053 */ 8054 async #getOverflowInfo() { 8055 function getInlineSize(aElement) { 8056 return aElement.getBoundingClientRect().width; 8057 } 8058 8059 function sumChildrenInlineSize(aParent, aExceptChild = null) { 8060 let sum = 0; 8061 for (let child of aParent.children) { 8062 let style = win.getComputedStyle(child); 8063 if ( 8064 style.display == "none" || 8065 win.XULPopupElement.isInstance(child) || 8066 (style.position != "static" && style.position != "relative") 8067 ) { 8068 continue; 8069 } 8070 sum += parseFloat(style.marginLeft) + parseFloat(style.marginRight); 8071 if (child != aExceptChild) { 8072 sum += getInlineSize(child); 8073 } 8074 } 8075 return sum; 8076 } 8077 8078 let win = this.#target.ownerGlobal; 8079 let totalAvailWidth; 8080 let targetWidth; 8081 let targetChildrenWidth; 8082 8083 await win.promiseDocumentFlushed(() => { 8084 let style = win.getComputedStyle(this.#toolbar); 8085 let toolbarChildrenWidth = sumChildrenInlineSize( 8086 this.#toolbar, 8087 this.#target 8088 ); 8089 totalAvailWidth = 8090 getInlineSize(this.#toolbar) - 8091 parseFloat(style.paddingLeft) - 8092 parseFloat(style.paddingRight) - 8093 toolbarChildrenWidth; 8094 targetWidth = getInlineSize(this.#target); 8095 targetChildrenWidth = 8096 this.#target == this.#toolbar 8097 ? toolbarChildrenWidth 8098 : sumChildrenInlineSize(this.#target); 8099 }); 8100 8101 lazy.log.debug( 8102 `Getting overflow info: target width: ${targetWidth} (${targetChildrenWidth}); avail: ${totalAvailWidth}` 8103 ); 8104 8105 // If the target has min-width: 0, their children might actually overflow 8106 // it, so check for both cases explicitly. 8107 // We don't care about <1px differences, so ceil the avail width and floor 8108 // the content width to deal with it. 8109 let targetContentWidth = Math.floor( 8110 Math.max(targetWidth, targetChildrenWidth) 8111 ); 8112 totalAvailWidth = Math.ceil(totalAvailWidth); 8113 let isOverflowing = targetContentWidth > totalAvailWidth; 8114 return { isOverflowing, targetContentWidth, totalAvailWidth }; 8115 } 8116 8117 /** 8118 * Tries to move toolbar items back to the toolbar from the overflow panel. 8119 * 8120 * @param {boolean} shouldMoveAllItems 8121 * Whether we should move everything (e.g. because we're being 8122 * disabled) 8123 * @param {number} [totalAvailWidth=undefined] 8124 * Optional; the width of the toolbar area in which we can put things. 8125 * Some consumers pass this to avoid reflows. 8126 * 8127 * While there are items in the list, this width won't change, and so 8128 * we can avoid flushing layout by providing it and/or caching it. 8129 * Note that if `shouldMoveAllItems` is true, we never need the width 8130 * anyway, and this value is ignored. 8131 * @returns {Promise<undefined>} 8132 * Resolves once moving of items has completed. 8133 */ 8134 async #moveItemsBackToTheirOrigin(shouldMoveAllItems, totalAvailWidth) { 8135 lazy.log.debug( 8136 `Attempting to move ${shouldMoveAllItems ? "all" : "some"} items back` 8137 ); 8138 let placements = gPlacements.get(this.#toolbar.id); 8139 let win = this.#target.ownerGlobal; 8140 let doc = this.#target.ownerDocument; 8141 let checkOverflowHandle = this.#checkOverflowHandle; 8142 8143 let overflowedItemStack = Array.from(this.#overflowedInfo.entries()); 8144 8145 for (let i = overflowedItemStack.length - 1; i >= 0; --i) { 8146 let [childID, minSize] = overflowedItemStack[i]; 8147 8148 // The item may have been placed inside of a <xul:panel> that is lazily 8149 // loaded and still in the view cache. PanelMultiView.getViewNode will 8150 // do the work of checking the DOM for the child, and then falling back to 8151 // the cache if that is the case. 8152 let child = lazy.PanelMultiView.getViewNode(doc, childID); 8153 8154 if (!child) { 8155 this.#overflowedInfo.delete(childID); 8156 continue; 8157 } 8158 8159 lazy.log.debug( 8160 `Considering moving ${child.id} back, minSize: ${minSize}` 8161 ); 8162 8163 if (!shouldMoveAllItems && minSize) { 8164 if (!totalAvailWidth) { 8165 ({ totalAvailWidth } = await this.#getOverflowInfo()); 8166 8167 // If the window has closed or if we re-enter because we were waiting 8168 // for layout, stop. 8169 if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) { 8170 lazy.log.debug("Window closed or #checkOverflow called again."); 8171 return; 8172 } 8173 } 8174 if (totalAvailWidth <= minSize) { 8175 lazy.log.debug( 8176 `Need ${minSize} but width is ${totalAvailWidth} so bailing` 8177 ); 8178 break; 8179 } 8180 } 8181 8182 lazy.log.debug(`Moving ${child.id} back`); 8183 this.#overflowedInfo.delete(child.id); 8184 let beforeNodeIndex = placements.indexOf(child.id) + 1; 8185 // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list, 8186 // we're inserting it at the end. This will mean first-in, first-out (more or less) 8187 // leading to as little change in order as possible. 8188 if (beforeNodeIndex == 0) { 8189 beforeNodeIndex = placements.length; 8190 } 8191 let inserted = false; 8192 for (; beforeNodeIndex < placements.length; beforeNodeIndex++) { 8193 let beforeNode = this.#target.getElementsByAttribute( 8194 "id", 8195 placements[beforeNodeIndex] 8196 )[0]; 8197 // Unfortunately, XUL add-ons can mess with nodes after they are inserted, 8198 // and this breaks the following code if the button isn't where we expect 8199 // it to be (ie not a child of the target). In this case, ignore the node. 8200 if (beforeNode && this.#target == beforeNode.parentElement) { 8201 this.#target.insertBefore(child, beforeNode); 8202 inserted = true; 8203 break; 8204 } 8205 } 8206 if (!inserted) { 8207 this.#target.appendChild(child); 8208 } 8209 child.removeAttribute("cui-anchorid"); 8210 child.removeAttribute("overflowedItem"); 8211 CustomizableUIInternal.ensureButtonContextMenu(child, this.#target); 8212 CustomizableUIInternal.notifyListeners( 8213 "onWidgetUnderflow", 8214 child, 8215 this.#target 8216 ); 8217 } 8218 8219 win.UpdateUrlbarSearchSplitterState(); 8220 8221 let defaultListItems = Array.from(this.#defaultList.children); 8222 if ( 8223 defaultListItems.every( 8224 item => 8225 CustomizableUI.isSpecialWidget(item.id) || 8226 this.#hiddenOverflowedNodes.has(item) 8227 ) 8228 ) { 8229 this.#toolbar.removeAttribute("overflowing"); 8230 } 8231 } 8232 8233 /** 8234 * Checks to see if there are overflowable items within the customization 8235 * target of the toolbar that should be moved into the overflow panel, and 8236 * if there are, moves them. 8237 * 8238 * Note that since this is an async function that can be called in bursts 8239 * by resize events on the window, this function is often re-called even 8240 * when a prior call hasn't yet resolved. In that situation, the older calls 8241 * resolve early without doing any work and leave any DOM manipulation to the 8242 * most recent call. 8243 * 8244 * This function is a no-op if the OverflowableToolbar is disabled or the 8245 * DOM fullscreen UI is currently being used. 8246 * 8247 * @returns {Promise<undefined>} 8248 * Resolves once any movement of toolbar items has completed. 8249 */ 8250 async #checkOverflow() { 8251 if (!this.#enabled) { 8252 return; 8253 } 8254 8255 let win = this.#target.ownerGlobal; 8256 if (win.document.documentElement.hasAttribute("inDOMFullscreen")) { 8257 // Toolbars are hidden and cannot be made visible in DOM fullscreen mode 8258 // so there's nothing to do here. 8259 return; 8260 } 8261 8262 let checkOverflowHandle = (this.#checkOverflowHandle = {}); 8263 8264 lazy.log.debug("Checking overflow"); 8265 let { isOverflowing, totalAvailWidth } = await this.#getOverflowInfo(); 8266 if (win.closed || this.#checkOverflowHandle != checkOverflowHandle) { 8267 return; 8268 } 8269 8270 if (isOverflowing) { 8271 await this.#onOverflow(); 8272 } else { 8273 await this.#moveItemsBackToTheirOrigin(false, totalAvailWidth); 8274 } 8275 8276 if (checkOverflowHandle == this.#checkOverflowHandle) { 8277 this.#checkOverflowHandle = null; 8278 } 8279 } 8280 8281 /** 8282 * Makes the OverflowableToolbar inert and moves all overflowable items back 8283 * into the customization target of the toolbar. 8284 */ 8285 #disable() { 8286 // Abort any ongoing overflow check. #enable() will #checkOverflow() 8287 // anyways, so this is enough. 8288 this.#checkOverflowHandle = {}; 8289 this.#moveItemsBackToTheirOrigin(true); 8290 this.#enabled = false; 8291 } 8292 8293 /** 8294 * Puts the OverflowableToolbar into the enabled state and then checks to see 8295 * if any of the items in the customization target should be overflowed into 8296 * the overflow panel list. 8297 */ 8298 #enable() { 8299 this.#enabled = true; 8300 this.#checkOverflow(); 8301 } 8302 8303 /** 8304 * Shows the overflow panel and sets a timeout to automatically re-hide the 8305 * panel if it is not being hovered. 8306 */ 8307 #showWithTimeout() { 8308 const OVERFLOW_PANEL_HIDE_DELAY_MS = 500; 8309 8310 this.show().then(() => { 8311 let window = this.#toolbar.ownerGlobal; 8312 if (this.#hideTimeoutId) { 8313 window.clearTimeout(this.#hideTimeoutId); 8314 } 8315 this.#hideTimeoutId = window.setTimeout(() => { 8316 if (!this.#defaultListPanel.firstElementChild.matches(":hover")) { 8317 lazy.PanelMultiView.hidePopup(this.#defaultListPanel); 8318 } 8319 }, OVERFLOW_PANEL_HIDE_DELAY_MS); 8320 }); 8321 } 8322 8323 /** 8324 * Gets and caches a reference to the DOM node with the ID set as the value 8325 * of addon-webext-overflowtarget. If a cache already exists, that's returned 8326 * instead. If addon-webext-overflowtarget has no value, null is returned. 8327 * 8328 * @returns {Element|null} the list that overflowed extension toolbar 8329 * buttons should go to if the Unified Extensions UI is enabled, or null 8330 * if no such list exists. 8331 */ 8332 get #webExtList() { 8333 if (!this.#webExtListRef) { 8334 let targetID = this.#toolbar.getAttribute("addon-webext-overflowtarget"); 8335 if (!targetID) { 8336 throw new Error( 8337 "addon-webext-overflowtarget was not defined on the " + 8338 `overflowable toolbar with id: ${this.#toolbar.id}` 8339 ); 8340 } 8341 let win = this.#toolbar.ownerGlobal; 8342 let { panel } = win.gUnifiedExtensions; 8343 this.#webExtListRef = panel.querySelector(`#${targetID}`); 8344 } 8345 return this.#webExtListRef; 8346 } 8347 8348 /** 8349 * Returns true if aNode is not null and is one of either this.#webExtList or 8350 * this.#defaultList. 8351 * 8352 * @param {DOMElement} aNode The node to test. 8353 * @returns {boolean} 8354 */ 8355 #isOverflowList(aNode) { 8356 return aNode == this.#defaultList || aNode == this.#webExtList; 8357 } 8358 8359 /** 8360 * Private event handlers start here. 8361 */ 8362 8363 /** 8364 * Handles clicks on the #defaultListButton element. 8365 * 8366 * @param {MouseEvent} aEvent the click event. 8367 */ 8368 #onClickDefaultListButton(aEvent) { 8369 if (this.#defaultListButton.open) { 8370 this.#defaultListButton.open = false; 8371 lazy.PanelMultiView.hidePopup(this.#defaultListPanel); 8372 } else if ( 8373 this.#defaultListPanel.state != "hiding" && 8374 !this.#defaultListButton.disabled 8375 ) { 8376 this.show(aEvent); 8377 } 8378 } 8379 8380 /** 8381 * Handles the popuphiding event firing on the #defaultListPanel. 8382 * 8383 * @param {WidgetMouseEvent} aEvent the popuphiding event that fired on the 8384 * #defaultListPanel. 8385 */ 8386 #onPanelHiding(aEvent) { 8387 if (aEvent.target != this.#defaultListPanel) { 8388 // Ignore context menus, <select> popups, etc. 8389 return; 8390 } 8391 this.#defaultListButton.open = false; 8392 this.#defaultListPanel.removeEventListener("dragover", this); 8393 this.#defaultListPanel.removeEventListener("dragend", this); 8394 let doc = aEvent.target.ownerDocument; 8395 doc.defaultView.updateEditUIVisibility(); 8396 let contextMenuId = this.#defaultListPanel.getAttribute("context"); 8397 if (contextMenuId) { 8398 let contextMenu = doc.getElementById(contextMenuId); 8399 contextMenu.removeEventListener("command", this, { 8400 capture: true, 8401 mozSystemGroup: true, 8402 }); 8403 } 8404 } 8405 8406 /** 8407 * Handles a resize event fired on the window hosting this 8408 * OverflowableToolbar. 8409 * 8410 * @param {UIEvent} aEvent the resize event. 8411 */ 8412 #onResize(aEvent) { 8413 // Ignore bubbled-up resize events. 8414 if (aEvent.target != aEvent.currentTarget) { 8415 return; 8416 } 8417 this.#checkOverflow(); 8418 } 8419 8420 /** 8421 * CustomizableUI listener methods start here. 8422 */ 8423 8424 onWidgetBeforeDOMChange(aNode, aNextNode, aContainer) { 8425 // This listener method is used to handle the case where a widget is 8426 // moved or removed from an area via the CustomizableUI API while 8427 // overflowed. It reorganizes the internal state of this OverflowableToolbar 8428 // to handle that change. 8429 if (!this.#enabled || !this.#isOverflowList(aContainer)) { 8430 return; 8431 } 8432 // When we (re)move an item, update all the items that come after it in the list 8433 // with the minsize *of the item before the to-be-removed node*. This way, we 8434 // ensure that we try to move items back as soon as that's possible. 8435 let updatedMinSize; 8436 if (aNode.previousElementSibling) { 8437 updatedMinSize = this.#overflowedInfo.get( 8438 aNode.previousElementSibling.id 8439 ); 8440 } else { 8441 // Force (these) items to try to flow back into the bar: 8442 updatedMinSize = 1; 8443 } 8444 let nextItem = aNode.nextElementSibling; 8445 while (nextItem) { 8446 this.#overflowedInfo.set(nextItem.id, updatedMinSize); 8447 nextItem = nextItem.nextElementSibling; 8448 } 8449 } 8450 8451 onWidgetAfterDOMChange(aNode, aNextNode, aContainer) { 8452 // This listener method is used to handle the case where a widget is 8453 // moved or removed from an area via the CustomizableUI API while 8454 // overflowed. It updates the DOM in the event that the movement or removal 8455 // causes overflow or underflow of the toolbar. 8456 if ( 8457 !this.#enabled || 8458 (aContainer != this.#target && !this.#isOverflowList(aContainer)) 8459 ) { 8460 return; 8461 } 8462 8463 let nowOverflowed = this.#isOverflowList(aNode.parentNode); 8464 let wasOverflowed = this.#overflowedInfo.has(aNode.id); 8465 8466 // If this wasn't overflowed before... 8467 if (!wasOverflowed) { 8468 // ... but it is now, then we added to one of the overflow panels. 8469 if (nowOverflowed) { 8470 // We could be the first item in the overflow panel if we're being inserted 8471 // before the previous first item in it. We can't assume the minimum 8472 // size is the same (because the other item might be much wider), so if 8473 // there is no previous item, just allow this item to be put back in the 8474 // toolbar immediately by specifying a very low minimum size. 8475 let sourceOfMinSize = aNode.previousElementSibling; 8476 let minSize = sourceOfMinSize 8477 ? this.#overflowedInfo.get(sourceOfMinSize.id) 8478 : 1; 8479 this.#overflowedInfo.set(aNode.id, minSize); 8480 aNode.setAttribute("cui-anchorid", this.#defaultListButton.id); 8481 aNode.setAttribute("overflowedItem", true); 8482 CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer, true); 8483 CustomizableUIInternal.notifyListeners( 8484 "onWidgetOverflow", 8485 aNode, 8486 this.#target 8487 ); 8488 } 8489 } else if (!nowOverflowed) { 8490 // If it used to be overflowed... 8491 // ... and isn't anymore, let's remove our bookkeeping: 8492 this.#overflowedInfo.delete(aNode.id); 8493 aNode.removeAttribute("cui-anchorid"); 8494 aNode.removeAttribute("overflowedItem"); 8495 CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer); 8496 CustomizableUIInternal.notifyListeners( 8497 "onWidgetUnderflow", 8498 aNode, 8499 this.#target 8500 ); 8501 8502 let collapsedWidgetIds = Array.from(this.#overflowedInfo.keys()); 8503 if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) { 8504 this.#toolbar.removeAttribute("overflowing"); 8505 } 8506 } else if (aNode.previousElementSibling) { 8507 // but if it still is, it must have changed places. Bookkeep: 8508 let prevId = aNode.previousElementSibling.id; 8509 let minSize = this.#overflowedInfo.get(prevId); 8510 this.#overflowedInfo.set(aNode.id, minSize); 8511 } 8512 8513 // We might overflow now if an item was added, or we may be able to move 8514 // stuff back into the toolbar if an item was removed. 8515 this.#checkOverflow(); 8516 } 8517 8518 /** 8519 * @returns {boolean} whether the given node is in the overflow list. 8520 */ 8521 isInOverflowList(node) { 8522 return node.parentNode == this.#defaultList; 8523 } 8524 8525 /** 8526 * nsIObserver implementation starts here. 8527 */ 8528 8529 observe(aSubject, aTopic) { 8530 // This nsIObserver method allows us to defer initialization until after 8531 // this window has finished painting and starting up. 8532 if ( 8533 aTopic == "browser-delayed-startup-finished" && 8534 aSubject == this.#toolbar.ownerGlobal 8535 ) { 8536 Services.obs.removeObserver(this, "browser-delayed-startup-finished"); 8537 this.init(); 8538 } 8539 } 8540 8541 /** 8542 * nsIDOMEventListener implementation starts here. 8543 */ 8544 8545 handleEvent(aEvent) { 8546 switch (aEvent.type) { 8547 case "aftercustomization": { 8548 this.#enable(); 8549 break; 8550 } 8551 case "mousedown": { 8552 if (aEvent.button != 0) { 8553 break; 8554 } 8555 if (aEvent.target == this.#defaultListButton) { 8556 this.#onClickDefaultListButton(aEvent); 8557 } else { 8558 lazy.PanelMultiView.hidePopup(this.#defaultListPanel); 8559 } 8560 break; 8561 } 8562 case "keypress": { 8563 if ( 8564 aEvent.target == this.#defaultListButton && 8565 (aEvent.key == " " || aEvent.key == "Enter") 8566 ) { 8567 this.#onClickDefaultListButton(aEvent); 8568 } 8569 break; 8570 } 8571 case "customizationstarting": { 8572 this.#disable(); 8573 break; 8574 } 8575 case "dragover": { 8576 if (this.#enabled) { 8577 this.#showWithTimeout(); 8578 } 8579 break; 8580 } 8581 case "dragend": { 8582 lazy.PanelMultiView.hidePopup(this.#defaultListPanel); 8583 break; 8584 } 8585 case "popuphiding": { 8586 this.#onPanelHiding(aEvent); 8587 break; 8588 } 8589 case "resize": { 8590 this.#onResize(aEvent); 8591 break; 8592 } 8593 } 8594 } 8595 } 8596 8597 CustomizableUIInternal.initialize();