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