Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  CustomizeMode.sys.mjs   Sprache: unbekannt

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPaletteId = "customization-palette";
const kDragDataTypePrefix = "text/toolbarwrapper-id/";
const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
const kDrawInTitlebarPref = "browser.tabs.inTitlebar";
const kCompactModeShowPref = "browser.compactmode.show";
const kBookmarksToolbarPref = "browser.toolbars.bookmarks.visibility";
const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";

const kPanelItemContextMenu = "customizationPanelItemContextMenu";
const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";

const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
const kDownloadAutoHidePref = "browser.download.autohideButton";

import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
  DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
  URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
  const kUrl =
    "chrome://browser/locale/customizableui/customizableWidgets.properties";
  return Services.strings.createBundle(kUrl);
});
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "gTouchBarUpdater",
  "@mozilla.org/widget/touchbarupdater;1",
  "nsITouchBarUpdater"
);

let gDebug;
ChromeUtils.defineLazyGetter(lazy, "log", () => {
  let { ConsoleAPI } = ChromeUtils.importESModule(
    "resource://gre/modules/Console.sys.mjs"
  );
  gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
  let consoleOptions = {
    maxLogLevel: gDebug ? "all" : "log",
    prefix: "CustomizeMode",
  };
  return new ConsoleAPI(consoleOptions);
});

var gDraggingInToolbars;

var gTab;

function closeGlobalTab() {
  let win = gTab.ownerGlobal;
  if (win.gBrowser.browsers.length == 1) {
    win.BrowserCommands.openTab();
  }
  win.gBrowser.removeTab(gTab, { animate: true });
  gTab = null;
}

var gTabsProgressListener = {
  onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) {
    // Tear down customize mode when the customize mode tab loads some other page.
    // Customize mode will be re-entered if "about:blank" is loaded again, so
    // don't tear down in this case.
    if (
      !gTab ||
      gTab.linkedBrowser != aBrowser ||
      aLocation.spec == "about:blank"
    ) {
      return;
    }

    unregisterGlobalTab();
  },
};

function unregisterGlobalTab() {
  gTab.removeEventListener("TabClose", unregisterGlobalTab);
  let win = gTab.ownerGlobal;
  win.removeEventListener("unload", unregisterGlobalTab);
  win.gBrowser.removeTabsProgressListener(gTabsProgressListener);

  gTab.removeAttribute("customizemode");

  gTab = null;
}

export function CustomizeMode(aWindow) {
  this.window = aWindow;
  this.document = aWindow.document;
  this.browser = aWindow.gBrowser;
  this.areas = new Set();

  this._translationObserver = new aWindow.MutationObserver(mutations =>
    this._onTranslations(mutations)
  );
  this._ensureCustomizationPanels();

  let content = this.$("customization-content-container");
  if (!content) {
    this.window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl");
    let container = this.$("customization-container");
    container.replaceChild(
      this.window.MozXULElement.parseXULToFragment(container.firstChild.data),
      container.lastChild
    );
  }

  this._attachEventListeners();

  // There are two palettes - there's the palette that can be overlayed with
  // toolbar items in browser.xhtml. This is invisible, and never seen by the
  // user. Then there's the visible palette, which gets populated and displayed
  // to the user when in customizing mode.
  this.visiblePalette = this.$(kPaletteId);
  this.pongArena = this.$("customization-pong-arena");

  if (this._canDrawInTitlebar()) {
    this._updateTitlebarCheckbox();
    Services.prefs.addObserver(kDrawInTitlebarPref, this);
  } else {
    this.$("customization-titlebar-visibility-checkbox").hidden = true;
  }

  // Observe pref changes to the bookmarks toolbar visibility,
  // since we won't get a toolbarvisibilitychange event if the
  // toolbar is changing from 'newtab' to 'always' in Customize mode
  // since the toolbar is shown with the 'newtab' setting.
  Services.prefs.addObserver(kBookmarksToolbarPref, this);

  this.window.addEventListener("unload", this);
}

CustomizeMode.prototype = {
  _changed: false,
  _transitioning: false,
  window: null,
  document: null,
  // areas is used to cache the customizable areas when in customization mode.
  areas: null,
  // When in customizing mode, we swap out the reference to the invisible
  // palette in gNavToolbox.palette for our visiblePalette. This way, for the
  // customizing browser window, when widgets are removed from customizable
  // areas and added to the palette, they're added to the visible palette.
  // _stowedPalette is a reference to the old invisible palette so we can
  // restore gNavToolbox.palette to its original state after exiting
  // customization mode.
  _stowedPalette: null,
  _dragOverItem: null,
  _customizing: false,
  _skipSourceNodeCheck: null,
  _mainViewContext: null,

  // These are the commands we continue to leave enabled while in customize mode.
  // All other commands are disabled, and we remove the disabled attribute when
  // leaving customize mode.
  _enabledCommands: new Set([
    "cmd_newNavigator",
    "cmd_newNavigatorTab",
    "cmd_newNavigatorTabNoEvent",
    "cmd_close",
    "cmd_closeWindow",
    "cmd_minimizeWindow",
    "cmd_quitApplication",
    "View:FullScreen",
    "Browser:NextTab",
    "Browser:PrevTab",
    "Browser:NewUserContextTab",
    "Tools:PrivateBrowsing",
    "zoomWindow",
  ]),

  get _handler() {
    return this.window.CustomizationHandler;
  },

  uninit() {
    if (this._canDrawInTitlebar()) {
      Services.prefs.removeObserver(kDrawInTitlebarPref, this);
    }
    Services.prefs.removeObserver(kBookmarksToolbarPref, this);
  },

  $(id) {
    return this.document.getElementById(id);
  },

  toggle() {
    if (
      this._handler.isEnteringCustomizeMode ||
      this._handler.isExitingCustomizeMode
    ) {
      this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
      return;
    }
    if (this._customizing) {
      this.exit();
    } else {
      this.enter();
    }
  },

  setTab(aTab) {
    if (gTab == aTab) {
      return;
    }

    if (gTab) {
      closeGlobalTab();
    }

    gTab = aTab;

    gTab.setAttribute("customizemode", "true");

    if (gTab.linkedPanel) {
      gTab.linkedBrowser.stop();
    }

    let win = gTab.ownerGlobal;

    win.gBrowser.setTabTitle(gTab);
    win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg");

    gTab.addEventListener("TabClose", unregisterGlobalTab);

    win.gBrowser.addTabsProgressListener(gTabsProgressListener);

    win.addEventListener("unload", unregisterGlobalTab);

    if (gTab.selected) {
      win.gCustomizeMode.enter();
    }
  },

  enter() {
    if (!this.window.toolbar.visible) {
      let w = lazy.URILoadingHelper.getTargetWindow(this.window, {
        skipPopups: true,
      });
      if (w) {
        w.gCustomizeMode.enter();
        return;
      }
      let obs = () => {
        Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
        w = lazy.URILoadingHelper.getTargetWindow(this.window, {
          skipPopups: true,
        });
        w.gCustomizeMode.enter();
      };
      Services.obs.addObserver(obs, "browser-delayed-startup-finished");
      this.window.openTrustedLinkIn("about:newtab", "window");
      return;
    }
    this._wantToBeInCustomizeMode = true;

    if (this._customizing || this._handler.isEnteringCustomizeMode) {
      return;
    }

    // Exiting; want to re-enter once we've done that.
    if (this._handler.isExitingCustomizeMode) {
      lazy.log.debug(
        "Attempted to enter while we're in the middle of exiting. " +
          "We'll exit after we've entered"
      );
      return;
    }

    if (!gTab) {
      this.setTab(
        this.browser.addTab("about:blank", {
          inBackground: false,
          forceNotRemote: true,
          skipAnimation: true,
          triggeringPrincipal:
            Services.scriptSecurityManager.getSystemPrincipal(),
        })
      );
      return;
    }
    if (!gTab.selected) {
      // This will force another .enter() to be called via the
      // onlocationchange handler of the tabbrowser, so we return early.
      gTab.ownerGlobal.gBrowser.selectedTab = gTab;
      return;
    }
    gTab.ownerGlobal.focus();
    if (gTab.ownerDocument != this.document) {
      return;
    }

    let window = this.window;
    let document = this.document;

    this._handler.isEnteringCustomizeMode = true;

    // Always disable the reset button at the start of customize mode, it'll be re-enabled
    // if necessary when we finish entering:
    let resetButton = this.$("customization-reset-button");
    resetButton.setAttribute("disabled", "true");

    (async () => {
      // We shouldn't start customize mode until after browser-delayed-startup has finished:
      if (!this.window.gBrowserInit.delayedStartupFinished) {
        await new Promise(resolve => {
          let delayedStartupObserver = aSubject => {
            if (aSubject == this.window) {
              Services.obs.removeObserver(
                delayedStartupObserver,
                "browser-delayed-startup-finished"
              );
              resolve();
            }
          };

          Services.obs.addObserver(
            delayedStartupObserver,
            "browser-delayed-startup-finished"
          );
        });
      }

      CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
      CustomizableUI.notifyStartCustomizing(this.window);

      // Add a keypress listener to the document so that we can quickly exit
      // customization mode when pressing ESC.
      document.addEventListener("keypress", this);

      // Same goes for the menu button - if we're customizing, a click on the
      // menu button means a quick exit from customization mode.
      window.PanelUI.hide();

      let panelHolder = document.getElementById("customization-panelHolder");
      let panelContextMenu = document.getElementById(kPanelItemContextMenu);
      this._previousPanelContextMenuParent = panelContextMenu.parentNode;
      document.getElementById("mainPopupSet").appendChild(panelContextMenu);
      panelHolder.appendChild(window.PanelUI.overflowFixedList);

      window.PanelUI.overflowFixedList.toggleAttribute("customizing", true);
      window.PanelUI.menuButton.disabled = true;
      document.getElementById("nav-bar-overflow-button").disabled = true;

      this._transitioning = true;

      let customizer = document.getElementById("customization-container");
      let browser = document.getElementById("browser");
      browser.hidden = true;
      customizer.hidden = false;

      this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP);

      this.document.documentElement.toggleAttribute("customizing", true);

      let customizableToolbars = document.querySelectorAll(
        "toolbar[customizable=true]:not([autohide=true], [collapsed=true])"
      );
      for (let toolbar of customizableToolbars) {
        toolbar.toggleAttribute("customizing", true);
      }

      this._updateOverflowPanelArrowOffset();

      // Let everybody in this window know that we're about to customize.
      CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);

      await this._wrapToolbarItems();
      this.populatePalette();

      this._setupPaletteDragging();

      window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);

      this._updateResetButton();
      this._updateUndoResetButton();
      this._updateTouchBarButton();
      this._updateDensityMenu();

      this._skipSourceNodeCheck =
        Services.prefs.getPrefType(kSkipSourceNodePref) ==
          Ci.nsIPrefBranch.PREF_BOOL &&
        Services.prefs.getBoolPref(kSkipSourceNodePref);

      CustomizableUI.addListener(this);
      this._customizing = true;
      this._transitioning = false;

      // Show the palette now that the transition has finished.
      this.visiblePalette.hidden = false;
      window.setTimeout(() => {
        // Force layout reflow to ensure the animation runs,
        // and make it async so it doesn't affect the timing.
        this.visiblePalette.clientTop;
        this.visiblePalette.setAttribute("showing", "true");
      }, 0);
      this._updateEmptyPaletteNotice();

      lazy.AddonManager.addAddonListener(this);

      this._setupDownloadAutoHideToggle();

      this._handler.isEnteringCustomizeMode = false;

      CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);

      if (!this._wantToBeInCustomizeMode) {
        this.exit();
      }
    })().catch(e => {
      lazy.log.error("Error entering customize mode", e);
      this._handler.isEnteringCustomizeMode = false;
      // Exit customize mode to ensure proper clean-up when entering failed.
      this.exit();
    });
  },

  exit() {
    this._wantToBeInCustomizeMode = false;

    if (!this._customizing || this._handler.isExitingCustomizeMode) {
      return;
    }

    // Entering; want to exit once we've done that.
    if (this._handler.isEnteringCustomizeMode) {
      lazy.log.debug(
        "Attempted to exit while we're in the middle of entering. " +
          "We'll exit after we've entered"
      );
      return;
    }

    if (this.resetting) {
      lazy.log.debug(
        "Attempted to exit while we're resetting. " +
          "We'll exit after resetting has finished."
      );
      return;
    }

    this._handler.isExitingCustomizeMode = true;

    this._translationObserver.disconnect();

    this._teardownDownloadAutoHideToggle();

    lazy.AddonManager.removeAddonListener(this);
    CustomizableUI.removeListener(this);

    let window = this.window;
    let document = this.document;

    document.removeEventListener("keypress", this);

    this.togglePong(false);

    // Disable the reset and undo reset buttons while transitioning:
    let resetButton = this.$("customization-reset-button");
    let undoResetButton = this.$("customization-undo-reset-button");
    undoResetButton.hidden = resetButton.disabled = true;

    this._transitioning = true;

    this._depopulatePalette();

    // We need to set this._customizing to false and remove the `customizing`
    // attribute before removing the tab or else
    // XULBrowserWindow.onLocationChange might think that we're still in
    // customization mode and need to exit it for a second time.
    this._customizing = false;
    document.documentElement.removeAttribute("customizing");

    if (this.browser.selectedTab == gTab) {
      closeGlobalTab();
    }

    let customizer = document.getElementById("customization-container");
    let browser = document.getElementById("browser");
    customizer.hidden = true;
    browser.hidden = false;

    window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);

    this._teardownPaletteDragging();

    (async () => {
      await this._unwrapToolbarItems();

      // And drop all area references.
      this.areas.clear();

      // Let everybody in this window know that we're starting to
      // exit customization mode.
      CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);

      window.PanelUI.menuButton.disabled = false;
      let overflowContainer = document.getElementById(
        "widget-overflow-mainView"
      ).firstElementChild;
      overflowContainer.appendChild(window.PanelUI.overflowFixedList);
      document.getElementById("nav-bar-overflow-button").disabled = false;
      let panelContextMenu = document.getElementById(kPanelItemContextMenu);
      this._previousPanelContextMenuParent.appendChild(panelContextMenu);

      let customizableToolbars = document.querySelectorAll(
        "toolbar[customizable=true]:not([autohide=true])"
      );
      for (let toolbar of customizableToolbars) {
        toolbar.removeAttribute("customizing");
      }

      this._maybeMoveDownloadsButtonToNavBar();

      delete this._lastLightweightTheme;
      this._changed = false;
      this._transitioning = false;
      this._handler.isExitingCustomizeMode = false;
      CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
      CustomizableUI.notifyEndCustomizing(window);

      if (this._wantToBeInCustomizeMode) {
        this.enter();
      }
    })().catch(e => {
      lazy.log.error("Error exiting customize mode", e);
      this._handler.isExitingCustomizeMode = false;
    });
  },

  /**
   * The overflow panel in customize mode should have its arrow pointing
   * at the overflow button. In order to do this correctly, we pass the
   * distance between the inside of window and the middle of the button
   * to the customize mode markup in which the arrow and panel are placed.
   */
  async _updateOverflowPanelArrowOffset() {
    let currentDensity =
      this.document.documentElement.getAttribute("uidensity");
    let offset = await this.window.promiseDocumentFlushed(() => {
      let overflowButton = this.$("nav-bar-overflow-button");
      let buttonRect = overflowButton.getBoundingClientRect();
      let endDistance;
      if (this.window.RTL_UI) {
        endDistance = buttonRect.left;
      } else {
        endDistance = this.window.innerWidth - buttonRect.right;
      }
      return endDistance + buttonRect.width / 2;
    });
    if (
      !this.document ||
      currentDensity != this.document.documentElement.getAttribute("uidensity")
    ) {
      return;
    }
    this.$("customization-panelWrapper").style.setProperty(
      "--panel-arrow-offset",
      offset + "px"
    );
  },

  _getCustomizableChildForNode(aNode) {
    // NB: adjusted from _getCustomizableParent to keep that method fast
    // (it's used during drags), and avoid multiple DOM loops
    let areas = CustomizableUI.areas;
    // Caching this length is important because otherwise we'll also iterate
    // over items we add to the end from within the loop.
    let numberOfAreas = areas.length;
    for (let i = 0; i < numberOfAreas; i++) {
      let area = areas[i];
      let areaNode = aNode.ownerDocument.getElementById(area);
      let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode);
      if (customizationTarget && customizationTarget != areaNode) {
        areas.push(customizationTarget.id);
      }
      let overflowTarget =
        areaNode && areaNode.getAttribute("default-overflowtarget");
      if (overflowTarget) {
        areas.push(overflowTarget);
      }
    }
    areas.push(kPaletteId);

    while (aNode && aNode.parentNode) {
      let parent = aNode.parentNode;
      if (areas.includes(parent.id)) {
        return aNode;
      }
      aNode = parent;
    }
    return null;
  },

  _promiseWidgetAnimationOut(aNode) {
    if (
      this.window.gReduceMotion ||
      aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" ||
      (aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") ||
      (aNode.id == "downloads-button" && aNode.hidden)
    ) {
      return null;
    }

    let animationNode;
    if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
      animationNode = aNode.parentNode;
    } else {
      animationNode = aNode;
    }
    return new Promise(resolve => {
      function cleanupCustomizationExit() {
        resolveAnimationPromise();
      }

      function cleanupWidgetAnimationEnd(e) {
        if (
          e.animationName == "widget-animate-out" &&
          e.target.id == animationNode.id
        ) {
          resolveAnimationPromise();
        }
      }

      function resolveAnimationPromise() {
        animationNode.removeEventListener(
          "animationend",
          cleanupWidgetAnimationEnd
        );
        animationNode.removeEventListener(
          "customizationending",
          cleanupCustomizationExit
        );
        resolve(animationNode);
      }

      // Wait until the next frame before setting the class to ensure
      // we do start the animation.
      this.window.requestAnimationFrame(() => {
        this.window.requestAnimationFrame(() => {
          animationNode.classList.add("animate-out");
          animationNode.ownerGlobal.gNavToolbox.addEventListener(
            "customizationending",
            cleanupCustomizationExit
          );
          animationNode.addEventListener(
            "animationend",
            cleanupWidgetAnimationEnd
          );
        });
      });
    });
  },

  async addToToolbar(aNode) {
    aNode = this._getCustomizableChildForNode(aNode);
    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
      aNode = aNode.firstElementChild;
    }
    let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
    let animationNode;
    if (widgetAnimationPromise) {
      animationNode = await widgetAnimationPromise;
    }

    let widgetToAdd = aNode.id;
    if (
      CustomizableUI.isSpecialWidget(widgetToAdd) &&
      aNode.closest("#customization-palette")
    ) {
      widgetToAdd = widgetToAdd.match(
        /^customizableui-special-(spring|spacer|separator)/
      )[1];
    }

    CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
    lazy.BrowserUsageTelemetry.recordWidgetChange(
      widgetToAdd,
      CustomizableUI.AREA_NAVBAR
    );
    if (!this._customizing) {
      CustomizableUI.dispatchToolboxEvent("customizationchange");
    }

    // If the user explicitly moves this item, turn off autohide.
    if (aNode.id == "downloads-button") {
      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
      if (this._customizing) {
        this._showDownloadsAutoHidePanel();
      }
    }

    if (animationNode) {
      animationNode.classList.remove("animate-out");
    }
  },

  async addToPanel(aNode, aReason) {
    aNode = this._getCustomizableChildForNode(aNode);
    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
      aNode = aNode.firstElementChild;
    }
    let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
    let animationNode;
    if (widgetAnimationPromise) {
      animationNode = await widgetAnimationPromise;
    }

    let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
    CustomizableUI.addWidgetToArea(aNode.id, panel);
    lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, panel, aReason);
    if (!this._customizing) {
      CustomizableUI.dispatchToolboxEvent("customizationchange");
    }

    // If the user explicitly moves this item, turn off autohide.
    if (aNode.id == "downloads-button") {
      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
      if (this._customizing) {
        this._showDownloadsAutoHidePanel();
      }
    }

    if (animationNode) {
      animationNode.classList.remove("animate-out");
    }
    if (!this.window.gReduceMotion) {
      let overflowButton = this.$("nav-bar-overflow-button");
      overflowButton.setAttribute("animate", "true");
      overflowButton.addEventListener(
        "animationend",
        function onAnimationEnd(event) {
          if (event.animationName.startsWith("overflow-animation")) {
            this.removeEventListener("animationend", onAnimationEnd);
            this.removeAttribute("animate");
          }
        }
      );
    }
  },

  async removeFromArea(aNode, aReason) {
    aNode = this._getCustomizableChildForNode(aNode);
    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
      aNode = aNode.firstElementChild;
    }
    let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
    let animationNode;
    if (widgetAnimationPromise) {
      animationNode = await widgetAnimationPromise;
    }

    CustomizableUI.removeWidgetFromArea(aNode.id);
    lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason);
    if (!this._customizing) {
      CustomizableUI.dispatchToolboxEvent("customizationchange");
    }

    // If the user explicitly removes this item, turn off autohide.
    if (aNode.id == "downloads-button") {
      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
      if (this._customizing) {
        this._showDownloadsAutoHidePanel();
      }
    }
    if (animationNode) {
      animationNode.classList.remove("animate-out");
    }
  },

  populatePalette() {
    let fragment = this.document.createDocumentFragment();
    let toolboxPalette = this.window.gNavToolbox.palette;

    try {
      let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
      for (let widget of unusedWidgets) {
        let paletteItem = this.makePaletteItem(widget, "palette");
        if (!paletteItem) {
          continue;
        }
        fragment.appendChild(paletteItem);
      }

      let flexSpace = CustomizableUI.createSpecialWidget(
        "spring",
        this.document
      );
      fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette"));

      this.visiblePalette.appendChild(fragment);
      this._stowedPalette = this.window.gNavToolbox.palette;
      this.window.gNavToolbox.palette = this.visiblePalette;

      // Now that the palette items are all here, disable all commands.
      // We do this here rather than directly in `enter` because we
      // need to do/undo this when we're called from reset(), too.
      this._updateCommandsDisabledState(true);
    } catch (ex) {
      lazy.log.error(ex);
    }
  },

  // XXXunf Maybe this should use -moz-element instead of wrapping the node?
  //       Would ensure no weird interactions/event handling from original node,
  //       and makes it possible to put this in a lazy-loaded iframe/real tab
  //       while still getting rid of the need for overlays.
  makePaletteItem(aWidget, aPlace) {
    let widgetNode = aWidget.forWindow(this.window).node;
    if (!widgetNode) {
      lazy.log.error(
        "Widget with id " + aWidget.id + " does not return a valid node"
      );
      return null;
    }
    // Do not build a palette item for hidden widgets; there's not much to show.
    if (widgetNode.hidden) {
      return null;
    }

    let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
    wrapper.appendChild(widgetNode);
    return wrapper;
  },

  _depopulatePalette() {
    // Quick, undo the command disabling before we depopulate completely:
    this._updateCommandsDisabledState(false);

    this.visiblePalette.hidden = true;
    let paletteChild = this.visiblePalette.firstElementChild;
    let nextChild;
    while (paletteChild) {
      nextChild = paletteChild.nextElementSibling;
      let itemId = paletteChild.firstElementChild.id;
      if (CustomizableUI.isSpecialWidget(itemId)) {
        this.visiblePalette.removeChild(paletteChild);
      } else {
        // XXXunf Currently this doesn't destroy the (now unused) node in the
        //       API provider case. It would be good to do so, but we need to
        //       keep strong refs to it in CustomizableUI (can't iterate of
        //       WeakMaps), and there's the question of what behavior
        //       wrappers should have if consumers keep hold of them.
        let unwrappedPaletteItem = this.unwrapToolbarItem(paletteChild);
        this._stowedPalette.appendChild(unwrappedPaletteItem);
      }

      paletteChild = nextChild;
    }
    this.visiblePalette.hidden = false;
    this.window.gNavToolbox.palette = this._stowedPalette;
  },

  _updateCommandsDisabledState(shouldBeDisabled) {
    for (let command of this.document.querySelectorAll("command")) {
      if (!command.id || !this._enabledCommands.has(command.id)) {
        if (shouldBeDisabled) {
          if (command.getAttribute("disabled") != "true") {
            command.setAttribute("disabled", true);
          } else {
            command.setAttribute("wasdisabled", true);
          }
        } else if (command.getAttribute("wasdisabled") != "true") {
          command.removeAttribute("disabled");
        } else {
          command.removeAttribute("wasdisabled");
        }
      }
    }
  },

  isCustomizableItem(aNode) {
    return (
      aNode.localName == "toolbarbutton" ||
      aNode.localName == "toolbaritem" ||
      aNode.localName == "toolbarseparator" ||
      aNode.localName == "toolbarspring" ||
      aNode.localName == "toolbarspacer"
    );
  },

  isWrappedToolbarItem(aNode) {
    return aNode.localName == "toolbarpaletteitem";
  },

  deferredWrapToolbarItem(aNode, aPlace) {
    return new Promise(resolve => {
      dispatchFunction(() => {
        let wrapper = this.wrapToolbarItem(aNode, aPlace);
        resolve(wrapper);
      });
    });
  },

  wrapToolbarItem(aNode, aPlace) {
    if (!this.isCustomizableItem(aNode)) {
      return aNode;
    }
    let wrapper = this.createOrUpdateWrapper(aNode, aPlace);

    // It's possible that this toolbar node is "mid-flight" and doesn't have
    // a parent, in which case we skip replacing it. This can happen if a
    // toolbar item has been dragged into the palette. In that case, we tell
    // CustomizableUI to remove the widget from its area before putting the
    // widget in the palette - so the node will have no parent.
    if (aNode.parentNode) {
      aNode = aNode.parentNode.replaceChild(wrapper, aNode);
    }
    wrapper.appendChild(aNode);
    return wrapper;
  },

  /**
   * Helper to set the label, either directly or to set up the translation
   * observer so we can set the label once it's available.
   */
  _updateWrapperLabel(aNode, aIsUpdate, aWrapper = aNode.parentElement) {
    if (aNode.hasAttribute("label")) {
      aWrapper.setAttribute("title", aNode.getAttribute("label"));
      aWrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
    } else if (aNode.hasAttribute("title")) {
      aWrapper.setAttribute("title", aNode.getAttribute("title"));
      aWrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
    } else if (aNode.hasAttribute("data-l10n-id") && !aIsUpdate) {
      this._translationObserver.observe(aNode, {
        attributes: true,
        attributeFilter: ["label", "title"],
      });
    }
  },

  /**
   * Called when a node without a label or title is updated.
   */
  _onTranslations(aMutations) {
    for (let mut of aMutations) {
      let { target } = mut;
      if (
        target.parentElement?.localName == "toolbarpaletteitem" &&
        (target.hasAttribute("label") || mut.target.hasAttribute("title"))
      ) {
        this._updateWrapperLabel(target, true);
      }
    }
  },

  createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
    let wrapper;
    if (
      aIsUpdate &&
      aNode.parentNode &&
      aNode.parentNode.localName == "toolbarpaletteitem"
    ) {
      wrapper = aNode.parentNode;
      aPlace = wrapper.getAttribute("place");
    } else {
      wrapper = this.document.createXULElement("toolbarpaletteitem");
      // "place" is used to show the label when it's sitting in the palette.
      wrapper.setAttribute("place", aPlace);
    }

    // Ensure the wrapped item doesn't look like it's in any special state, and
    // can't be interactved with when in the customization palette.
    // Note that some buttons opt out of this with the
    // keepbroadcastattributeswhencustomizing attribute.
    if (
      aNode.hasAttribute("command") &&
      aNode.getAttribute(kKeepBroadcastAttributes) != "true"
    ) {
      wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
      aNode.removeAttribute("command");
    }

    if (
      aNode.hasAttribute("observes") &&
      aNode.getAttribute(kKeepBroadcastAttributes) != "true"
    ) {
      wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
      aNode.removeAttribute("observes");
    }

    if (aNode.getAttribute("checked") == "true") {
      wrapper.setAttribute("itemchecked", "true");
      aNode.removeAttribute("checked");
    }

    if (aNode.hasAttribute("id")) {
      wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
    }

    this._updateWrapperLabel(aNode, aIsUpdate, wrapper);

    if (aNode.hasAttribute("flex")) {
      wrapper.setAttribute("flex", aNode.getAttribute("flex"));
    }

    let removable =
      aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
    wrapper.setAttribute("removable", removable);

    // Allow touch events to initiate dragging in customize mode.
    // This is only supported on Windows for now.
    wrapper.setAttribute("touchdownstartsdrag", "true");

    let contextMenuAttrName = "";
    if (aNode.getAttribute("context")) {
      contextMenuAttrName = "context";
    } else if (aNode.getAttribute("contextmenu")) {
      contextMenuAttrName = "contextmenu";
    }
    let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
    let contextMenuForPlace =
      aPlace == "panel" ? kPanelItemContextMenu : kPaletteItemContextMenu;
    if (aPlace != "toolbar") {
      wrapper.setAttribute("context", contextMenuForPlace);
    }
    // Only keep track of the menu if it is non-default.
    if (currentContextMenu && currentContextMenu != contextMenuForPlace) {
      aNode.setAttribute("wrapped-context", currentContextMenu);
      aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName);
      aNode.removeAttribute(contextMenuAttrName);
    } else if (currentContextMenu == contextMenuForPlace) {
      aNode.removeAttribute(contextMenuAttrName);
    }

    // Only add listeners for newly created wrappers:
    if (!aIsUpdate) {
      wrapper.addEventListener("mousedown", this);
      wrapper.addEventListener("mouseup", this);
    }

    if (CustomizableUI.isSpecialWidget(aNode.id)) {
      wrapper.setAttribute(
        "title",
        lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
      );
    }

    return wrapper;
  },

  deferredUnwrapToolbarItem(aWrapper) {
    return new Promise(resolve => {
      dispatchFunction(() => {
        let item = null;
        try {
          item = this.unwrapToolbarItem(aWrapper);
        } catch (ex) {
          console.error(ex);
        }
        resolve(item);
      });
    });
  },

  unwrapToolbarItem(aWrapper) {
    if (aWrapper.nodeName != "toolbarpaletteitem") {
      return aWrapper;
    }
    aWrapper.removeEventListener("mousedown", this);
    aWrapper.removeEventListener("mouseup", this);

    let place = aWrapper.getAttribute("place");

    let toolbarItem = aWrapper.firstElementChild;
    if (!toolbarItem) {
      lazy.log.error(
        "no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id
      );
      aWrapper.remove();
      return null;
    }

    if (aWrapper.hasAttribute("itemobserves")) {
      toolbarItem.setAttribute(
        "observes",
        aWrapper.getAttribute("itemobserves")
      );
    }

    if (aWrapper.hasAttribute("itemchecked")) {
      toolbarItem.checked = true;
    }

    if (aWrapper.hasAttribute("itemcommand")) {
      let commandID = aWrapper.getAttribute("itemcommand");
      toolbarItem.setAttribute("command", commandID);

      // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
      let command = this.$(commandID);
      if (command && command.hasAttribute("disabled")) {
        toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
      }
    }

    let wrappedContext = toolbarItem.getAttribute("wrapped-context");
    if (wrappedContext) {
      let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
      toolbarItem.setAttribute(contextAttrName, wrappedContext);
      toolbarItem.removeAttribute("wrapped-contextAttrName");
      toolbarItem.removeAttribute("wrapped-context");
    } else if (place == "panel") {
      toolbarItem.setAttribute("context", kPanelItemContextMenu);
    }

    if (aWrapper.parentNode) {
      aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
    }
    return toolbarItem;
  },

  async _wrapToolbarItem(aArea) {
    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
    if (!target || this.areas.has(target)) {
      return null;
    }

    this._addDragHandlers(target);
    for (let child of target.children) {
      if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
        await this.deferredWrapToolbarItem(
          child,
          CustomizableUI.getPlaceForItem(child)
        ).catch(lazy.log.error);
      }
    }
    this.areas.add(target);
    return target;
  },

  _wrapToolbarItemSync(aArea) {
    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
    if (!target || this.areas.has(target)) {
      return null;
    }

    this._addDragHandlers(target);
    try {
      for (let child of target.children) {
        if (
          this.isCustomizableItem(child) &&
          !this.isWrappedToolbarItem(child)
        ) {
          this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
        }
      }
    } catch (ex) {
      lazy.log.error(ex, ex.stack);
    }

    this.areas.add(target);
    return target;
  },

  async _wrapToolbarItems() {
    for (let area of CustomizableUI.areas) {
      await this._wrapToolbarItem(area);
    }
  },

  _addDragHandlers(aTarget) {
    // Allow dropping on the padding of the arrow panel.
    if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
      aTarget = this.$("customization-panelHolder");
    }
    aTarget.addEventListener("dragstart", this, true);
    aTarget.addEventListener("dragover", this, true);
    aTarget.addEventListener("dragleave", this, true);
    aTarget.addEventListener("drop", this, true);
    aTarget.addEventListener("dragend", this, true);
  },

  _wrapItemsInArea(target) {
    for (let child of target.children) {
      if (this.isCustomizableItem(child)) {
        this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
      }
    }
  },

  _removeDragHandlers(aTarget) {
    // Remove handler from different target if it was added to
    // allow dropping on the padding of the arrow panel.
    if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
      aTarget = this.$("customization-panelHolder");
    }
    aTarget.removeEventListener("dragstart", this, true);
    aTarget.removeEventListener("dragover", this, true);
    aTarget.removeEventListener("dragleave", this, true);
    aTarget.removeEventListener("drop", this, true);
    aTarget.removeEventListener("dragend", this, true);
  },

  _unwrapItemsInArea(target) {
    for (let toolbarItem of target.children) {
      if (this.isWrappedToolbarItem(toolbarItem)) {
        this.unwrapToolbarItem(toolbarItem);
      }
    }
  },

  _unwrapToolbarItems() {
    return (async () => {
      for (let target of this.areas) {
        for (let toolbarItem of target.children) {
          if (this.isWrappedToolbarItem(toolbarItem)) {
            await this.deferredUnwrapToolbarItem(toolbarItem);
          }
        }
        this._removeDragHandlers(target);
      }
      this.areas.clear();
    })().catch(lazy.log.error);
  },

  reset() {
    this.resetting = true;
    // Disable the reset button temporarily while resetting:
    let btn = this.$("customization-reset-button");
    btn.disabled = true;
    return (async () => {
      this._depopulatePalette();
      await this._unwrapToolbarItems();

      CustomizableUI.reset();

      await this._wrapToolbarItems();
      this.populatePalette();

      this._updateResetButton();
      this._updateUndoResetButton();
      this._updateEmptyPaletteNotice();
      this._moveDownloadsButtonToNavBar = false;
      this.resetting = false;
      if (!this._wantToBeInCustomizeMode) {
        this.exit();
      }
    })().catch(lazy.log.error);
  },

  undoReset() {
    this.resetting = true;

    return (async () => {
      this._depopulatePalette();
      await this._unwrapToolbarItems();

      CustomizableUI.undoReset();

      await this._wrapToolbarItems();
      this.populatePalette();

      this._updateResetButton();
      this._updateUndoResetButton();
      this._updateEmptyPaletteNotice();
      this._moveDownloadsButtonToNavBar = false;
      this.resetting = false;
    })().catch(lazy.log.error);
  },

  _onToolbarVisibilityChange(aEvent) {
    let toolbar = aEvent.target;
    toolbar.toggleAttribute(
      "customizing",
      aEvent.detail.visible && toolbar.getAttribute("customizable") == "true"
    );
    this._onUIChange();
  },

  onWidgetMoved() {
    this._onUIChange();
  },

  onWidgetAdded() {
    this._onUIChange();
  },

  onWidgetRemoved() {
    this._onUIChange();
  },

  onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
    if (aContainer.ownerGlobal != this.window || this.resetting) {
      return;
    }
    // If we get called for widgets that aren't in the window yet, they might not have
    // a parentNode at all.
    if (aNodeToChange.parentNode) {
      this.unwrapToolbarItem(aNodeToChange.parentNode);
    }
    if (aSecondaryNode) {
      this.unwrapToolbarItem(aSecondaryNode.parentNode);
    }
  },

  onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
    if (aContainer.ownerGlobal != this.window || this.resetting) {
      return;
    }
    // If the node is still attached to the container, wrap it again:
    if (aNodeToChange.parentNode) {
      let place = CustomizableUI.getPlaceForItem(aNodeToChange);
      this.wrapToolbarItem(aNodeToChange, place);
      if (aSecondaryNode) {
        this.wrapToolbarItem(aSecondaryNode, place);
      }
    } else {
      // If not, it got removed.

      // If an API-based widget is removed while customizing, append it to the palette.
      // The _applyDrop code itself will take care of positioning it correctly, if
      // applicable. We need the code to be here so removing widgets using CustomizableUI's
      // API also does the right thing (and adds it to the palette)
      let widgetId = aNodeToChange.id;
      let widget = CustomizableUI.getWidget(widgetId);
      if (widget.provider == CustomizableUI.PROVIDER_API) {
        let paletteItem = this.makePaletteItem(widget, "palette");
        this.visiblePalette.appendChild(paletteItem);
      }
    }
  },

  onWidgetDestroyed(aWidgetId) {
    let wrapper = this.$("wrapper-" + aWidgetId);
    if (wrapper) {
      wrapper.remove();
    }
  },

  onWidgetAfterCreation(aWidgetId, aArea) {
    // If the node was added to an area, we would have gotten an onWidgetAdded notification,
    // plus associated DOM change notifications, so only do stuff for the palette:
    if (!aArea) {
      let widgetNode = this.$(aWidgetId);
      if (widgetNode) {
        this.wrapToolbarItem(widgetNode, "palette");
      } else {
        let widget = CustomizableUI.getWidget(aWidgetId);
        this.visiblePalette.appendChild(
          this.makePaletteItem(widget, "palette")
        );
      }
    }
  },

  onAreaNodeRegistered(aArea, aContainer) {
    if (aContainer.ownerDocument == this.document) {
      this._wrapItemsInArea(aContainer);
      this._addDragHandlers(aContainer);
      this.areas.add(aContainer);
    }
  },

  onAreaNodeUnregistered(aArea, aContainer, aReason) {
    if (
      aContainer.ownerDocument == this.document &&
      aReason == CustomizableUI.REASON_AREA_UNREGISTERED
    ) {
      this._unwrapItemsInArea(aContainer);
      this._removeDragHandlers(aContainer);
      this.areas.delete(aContainer);
    }
  },

  openAddonsManagerThemes() {
    this.window.BrowserAddonUI.openAddonsMgr("addons://list/theme");
  },

  getMoreThemes(aEvent) {
    aEvent.target.parentNode.parentNode.hidePopup();
    let getMoreURL = Services.urlFormatter.formatURLPref(
      "lightweightThemes.getMoreURL"
    );
    this.window.openTrustedLinkIn(getMoreURL, "tab");
  },

  updateUIDensity(mode) {
    this.window.gUIDensity.update(mode);
    this._updateOverflowPanelArrowOffset();
  },

  setUIDensity(mode) {
    let win = this.window;
    let gUIDensity = win.gUIDensity;
    let currentDensity = gUIDensity.getCurrentDensity();
    let panel = win.document.getElementById("customization-uidensity-menu");

    Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode);

    // If the user is choosing a different UI density mode while
    // the mode is overriden to Touch, remove the override.
    if (currentDensity.overridden) {
      Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false);
    }

    this._onUIChange();
    panel.hidePopup();
    this._updateOverflowPanelArrowOffset();
  },

  resetUIDensity() {
    this.window.gUIDensity.update();
    this._updateOverflowPanelArrowOffset();
  },

  onUIDensityMenuShowing() {
    let win = this.window;
    let doc = win.document;
    let gUIDensity = win.gUIDensity;
    let currentDensity = gUIDensity.getCurrentDensity();

    let normalItem = doc.getElementById(
      "customization-uidensity-menuitem-normal"
    );
    normalItem.mode = gUIDensity.MODE_NORMAL;

    let items = [normalItem];

    let compactItem = doc.getElementById(
      "customization-uidensity-menuitem-compact"
    );
    compactItem.mode = gUIDensity.MODE_COMPACT;

    if (Services.prefs.getBoolPref(kCompactModeShowPref)) {
      compactItem.hidden = false;
      items.push(compactItem);
    } else {
      compactItem.hidden = true;
    }

    let touchItem = doc.getElementById(
      "customization-uidensity-menuitem-touch"
    );
    // Touch mode can not be enabled in OSX right now.
    if (touchItem) {
      touchItem.mode = gUIDensity.MODE_TOUCH;
      items.push(touchItem);
    }

    // Mark the active mode menuitem.
    for (let item of items) {
      if (item.mode == currentDensity.mode) {
        item.setAttribute("aria-checked", "true");
        item.setAttribute("active", "true");
      } else {
        item.removeAttribute("aria-checked");
        item.removeAttribute("active");
      }
    }

    // Add menu items for automatically switching to Touch mode in Windows Tablet Mode.
    if (AppConstants.platform == "win") {
      let spacer = doc.getElementById("customization-uidensity-touch-spacer");
      let checkbox = doc.getElementById(
        "customization-uidensity-autotouchmode-checkbox"
      );
      spacer.removeAttribute("hidden");
      checkbox.removeAttribute("hidden");

      // Show a hint that the UI density was overridden automatically.
      if (currentDensity.overridden) {
        let sb = Services.strings.createBundle(
          "chrome://browser/locale/uiDensity.properties"
        );
        touchItem.setAttribute(
          "acceltext",
          sb.GetStringFromName("uiDensity.menuitem-touch.acceltext")
        );
      } else {
        touchItem.removeAttribute("acceltext");
      }

      let autoTouchMode = Services.prefs.getBoolPref(
        win.gUIDensity.autoTouchModePref
      );
      if (autoTouchMode) {
        checkbox.setAttribute("checked", "true");
      } else {
        checkbox.removeAttribute("checked");
      }
    }
  },

  updateAutoTouchMode(checked) {
    Services.prefs.setBoolPref("browser.touchmode.auto", checked);
    // Re-render the menu items since the active mode might have
    // change because of this.
    this.onUIDensityMenuShowing();
    this._onUIChange();
  },

  _onUIChange() {
    this._changed = true;
    if (!this.resetting) {
      this._updateResetButton();
      this._updateUndoResetButton();
      this._updateEmptyPaletteNotice();
    }
    CustomizableUI.dispatchToolboxEvent("customizationchange");
  },

  _updateEmptyPaletteNotice() {
    let paletteItems =
      this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
    let whimsyButton = this.$("whimsy-button");

    if (
      paletteItems.length == 1 &&
      paletteItems[0].id.includes("wrapper-customizableui-special-spring")
    ) {
      whimsyButton.hidden = false;
    } else {
      this.togglePong(false);
      whimsyButton.hidden = true;
    }
  },

  _updateResetButton() {
    let btn = this.$("customization-reset-button");
    btn.disabled = CustomizableUI.inDefaultState;
  },

  _updateUndoResetButton() {
    let undoResetButton = this.$("customization-undo-reset-button");
    undoResetButton.hidden = !CustomizableUI.canUndoReset;
  },

  _updateTouchBarButton() {
    if (AppConstants.platform != "macosx") {
      return;
    }
    let touchBarButton = this.$("customization-touchbar-button");
    let touchBarSpacer = this.$("customization-touchbar-spacer");

    let isTouchBarInitialized = lazy.gTouchBarUpdater.isTouchBarInitialized();
    touchBarButton.hidden = !isTouchBarInitialized;
    touchBarSpacer.hidden = !isTouchBarInitialized;
  },

  _updateDensityMenu() {
    // If we're entering Customize Mode, and we're using compact mode,
    // then show the button after that.
    let gUIDensity = this.window.gUIDensity;
    if (gUIDensity.getCurrentDensity().mode == gUIDensity.MODE_COMPACT) {
      Services.prefs.setBoolPref(kCompactModeShowPref, true);
    }

    let button = this.document.getElementById("customization-uidensity-button");
    button.hidden =
      !Services.prefs.getBoolPref(kCompactModeShowPref) &&
      !button.querySelector("#customization-uidensity-menuitem-touch");
  },

  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "toolbarvisibilitychange":
        this._onToolbarVisibilityChange(aEvent);
        break;
      case "dragstart":
        this._onDragStart(aEvent);
        break;
      case "dragover":
        this._onDragOver(aEvent);
        break;
      case "drop":
        this._onDragDrop(aEvent);
        break;
      case "dragleave":
        this._onDragLeave(aEvent);
        break;
      case "dragend":
        this._onDragEnd(aEvent);
        break;
      case "mousedown":
        this._onMouseDown(aEvent);
        break;
      case "mouseup":
        this._onMouseUp(aEvent);
        break;
      case "keypress":
        if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
          this.exit();
        }
        break;
      case "unload":
        this.uninit();
        break;
    }
  },

  /**
   * We handle dragover/drop on the outer palette separately
   * to avoid overlap with other drag/drop handlers.
   */
  _setupPaletteDragging() {
    this._addDragHandlers(this.visiblePalette);

    this.paletteDragHandler = aEvent => {
      let originalTarget = aEvent.originalTarget;
      if (
        this._isUnwantedDragDrop(aEvent) ||
        this.visiblePalette.contains(originalTarget) ||
        this.$("customization-panelHolder").contains(originalTarget)
      ) {
        return;
      }
      // We have a dragover/drop on the palette.
      if (aEvent.type == "dragover") {
        this._onDragOver(aEvent, this.visiblePalette);
      } else {
        this._onDragDrop(aEvent, this.visiblePalette);
      }
    };
    let contentContainer = this.$("customization-content-container");
    contentContainer.addEventListener(
      "dragover",
      this.paletteDragHandler,
      true
    );
    contentContainer.addEventListener("drop", this.paletteDragHandler, true);
  },

  _teardownPaletteDragging() {
    lazy.DragPositionManager.stop();
    this._removeDragHandlers(this.visiblePalette);

    let contentContainer = this.$("customization-content-container");
    contentContainer.removeEventListener(
      "dragover",
      this.paletteDragHandler,
      true
    );
    contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
    delete this.paletteDragHandler;
  },

  observe(aSubject, aTopic) {
    switch (aTopic) {
      case "nsPref:changed":
        this._updateResetButton();
        this._updateUndoResetButton();
        if (this._canDrawInTitlebar()) {
          this._updateTitlebarCheckbox();
        }
        break;
    }
  },

  async onInstalled(addon) {
    await this.onEnabled(addon);
  },

  async onEnabled(addon) {
    if (addon.type != "theme") {
      return;
    }

    if (this._nextThemeChangeUserTriggered) {
      this._onUIChange();
    }
    this._nextThemeChangeUserTriggered = false;
  },

  _canDrawInTitlebar() {
    return this.window.CustomTitlebar.systemSupported;
  },

  _ensureCustomizationPanels() {
    let template = this.$("customizationPanel");
    template.replaceWith(template.content);

    let wrapper = this.$("customModeWrapper");
    wrapper.replaceWith(wrapper.content);
  },

  _attachEventListeners() {
    let container = this.$("customization-container");

    container.addEventListener("command", event => {
      switch (event.target.id) {
        case "customization-titlebar-visibility-checkbox":
          // NB: because command fires after click, by the time we've fired, the checkbox binding
          //     will already have switched the button's state, so this is correct:
          this.toggleTitlebar(event.target.checked);
          break;
        case "customization-uidensity-menuitem-compact":
        case "customization-uidensity-menuitem-normal":
        case "customization-uidensity-menuitem-touch":
          this.setUIDensity(event.target.mode);
          break;
        case "customization-uidensity-autotouchmode-checkbox":
          this.updateAutoTouchMode(event.target.checked);
          break;
        case "whimsy-button":
          this.togglePong(event.target.checked);
          break;
        case "customization-touchbar-button":
          this.customizeTouchBar();
          break;
        case "customization-undo-reset-button":
          this.undoReset();
          break;
        case "customization-reset-button":
          this.reset();
          break;
        case "customization-done-button":
          this.exit();
          break;
      }
    });

    container.addEventListener("popupshowing", event => {
      switch (event.target.id) {
        case "customization-toolbar-menu":
          this.window.ToolbarContextMenu.onViewToolbarsPopupShowing(event);
          break;
        case "customization-uidensity-menu":
          this.onUIDensityMenuShowing();
          break;
      }
    });

    let updateDensity = event => {
      switch (event.target.id) {
        case "customization-uidensity-menuitem-compact":
        case "customization-uidensity-menuitem-normal":
        case "customization-uidensity-menuitem-touch":
          this.updateUIDensity(event.target.mode);
      }
    };
    let densityMenu = this.document.getElementById(
      "customization-uidensity-menu"
    );
    densityMenu.addEventListener("focus", updateDensity);
    densityMenu.addEventListener("mouseover", updateDensity);

    let resetDensity = event => {
      switch (event.target.id) {
        case "customization-uidensity-menuitem-compact":
        case "customization-uidensity-menuitem-normal":
        case "customization-uidensity-menuitem-touch":
          this.resetUIDensity();
      }
    };
    densityMenu.addEventListener("blur", resetDensity);
    densityMenu.addEventListener("mouseout", resetDensity);

    this.$("customization-lwtheme-link").addEventListener("click", () => {
      this.openAddonsManagerThemes();
    });

    this.$(kPaletteItemContextMenu).addEventListener("popupshowing", event => {
      this.onPaletteContextMenuShowing(event);
    });

    this.$(kPaletteItemContextMenu).addEventListener("command", event => {
      switch (event.target.id) {
        case "customizationPaletteItemContextMenuAddToToolbar":
          this.addToToolbar(
            event.target.parentNode.triggerNode,
            "palette-context"
          );
          break;
        case "customizationPaletteItemContextMenuAddToPanel":
          this.addToPanel(
            event.target.parentNode.triggerNode,
            "palette-context"
          );
          break;
      }
    });

    let autohidePanel = this.$(kDownloadAutohidePanelId);
    autohidePanel.addEventListener("popupshown", event => {
      this._downloadPanelAutoHideTimeout = this.window.setTimeout(
        () => event.target.hidePopup(),
        4000
      );
    });
    autohidePanel.addEventListener("mouseover", () => {
      this.window.clearTimeout(this._downloadPanelAutoHideTimeout);
    });
    autohidePanel.addEventListener("mouseout", event => {
      this._downloadPanelAutoHideTimeout = this.window.setTimeout(
        () => event.target.hidePopup(),
        2000
      );
    });
    autohidePanel.addEventListener("popuphidden", () => {
      this.window.clearTimeout(this._downloadPanelAutoHideTimeout);
    });

    this.$(kDownloadAutohideCheckboxId).addEventListener("command", event => {
      this.onDownloadsAutoHideChange(event);
    });
  },

  _updateTitlebarCheckbox() {
    let drawInTitlebar = Services.appinfo.drawInTitlebar;
    let checkbox = this.$("customization-titlebar-visibility-checkbox");
    // Drawing in the titlebar means 'hiding' the titlebar.
    // We use the attribute rather than a property because if we're not in
    // customize mode the button is hidden and properties don't work.
    if (drawInTitlebar) {
      checkbox.removeAttribute("checked");
    } else {
      checkbox.setAttribute("checked", "true");
    }
  },

  toggleTitlebar(aShouldShowTitlebar) {
    // Drawing in the titlebar means not showing the titlebar, hence the negation:
    Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
  },

  _getBoundsWithoutFlushing(element) {
    return this.window.windowUtils.getBoundsWithoutFlushing(element);
  },

  _onDragStart(aEvent) {
    __dumpDragData(aEvent);
    let item = aEvent.target;
    while (item && item.localName != "toolbarpaletteitem") {
      if (
        item.localName == "toolbar" ||
        item.id == kPaletteId ||
        item.id == "customization-panelHolder"
      ) {
        return;
      }
      item = item.parentNode;
    }

    let draggedItem = item.firstElementChild;
    let placeForItem = CustomizableUI.getPlaceForItem(item);

    let dt = aEvent.dataTransfer;
    let documentId = aEvent.target.ownerDocument.documentElement.id;

    dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
    dt.effectAllowed = "move";

    let itemRect = this._getBoundsWithoutFlushing(draggedItem);
    let itemCenter = {
      x: itemRect.left + itemRect.width / 2,
      y: itemRect.top + itemRect.height / 2,
    };
    this._dragOffset = {
      x: aEvent.clientX - itemCenter.x,
      y: aEvent.clientY - itemCenter.y,
    };

    let toolbarParent = draggedItem.closest("toolbar");
    if (toolbarParent) {
      let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent);
      toolbarParent.style.minHeight = toolbarRect.height + "px";
    }

    gDraggingInToolbars = new Set();

    // Hack needed so that the dragimage will still show the
    // item as it appeared before it was hidden.
    this._initializeDragAfterMove = () => {
      // For automated tests, we sometimes start exiting customization mode
      // before this fires, which leaves us with placeholders inserted after
      // we've exited. So we need to check that we are indeed customizing.
      if (this._customizing && !this._transitioning) {
        item.hidden = true;
        lazy.DragPositionManager.start(this.window);
        let canUsePrevSibling =
          placeForItem == "toolbar" || placeForItem == "panel";
        if (item.nextElementSibling) {
          this._setDragActive(
            item.nextElementSibling,
            "before",
            draggedItem.id,
            placeForItem
          );
          this._dragOverItem = item.nextElementSibling;
        } else if (canUsePrevSibling && item.previousElementSibling) {
          this._setDragActive(
            item.previousElementSibling,
            "after",
            draggedItem.id,
            placeForItem
          );
          this._dragOverItem = item.previousElementSibling;
        }
        let currentArea = this._getCustomizableParent(item);
        currentArea.setAttribute("draggingover", "true");
      }
      this._initializeDragAfterMove = null;
      this.window.clearTimeout(this._dragInitializeTimeout);
    };
    this._dragInitializeTimeout = this.window.setTimeout(
      this._initializeDragAfterMove,
      0
    );
  },

  _onDragOver(aEvent, aOverrideTarget) {
    if (this._isUnwantedDragDrop(aEvent)) {
      return;
    }
    if (this._initializeDragAfterMove) {
      this._initializeDragAfterMove();
    }

    __dumpDragData(aEvent);

    let document = aEvent.target.ownerDocument;
    let documentId = document.documentElement.id;
    if (!aEvent.dataTransfer.mozTypesAt(0).length) {
      return;
    }

    let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
      kDragDataTypePrefix + documentId,
      0
    );
    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
    let targetArea = this._getCustomizableParent(
      aOverrideTarget || aEvent.currentTarget
    );
    let originArea = this._getCustomizableParent(draggedWrapper);

    // Do nothing if the target or origin are not customizable.
    if (!targetArea || !originArea) {
      return;
    }

    // Do nothing if the widget is not allowed to be removed.
    if (
      targetArea.id == kPaletteId &&
      !CustomizableUI.isWidgetRemovable(draggedItemId)
    ) {
      return;
    }

    // Do nothing if the widget is not allowed to move to the target area.
    if (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
      return;
    }

    let targetAreaType = CustomizableUI.getPlaceForItem(targetArea);
    let targetNode = this._getDragOverNode(
      aEvent,
      targetArea,
      targetAreaType,
      draggedItemId
    );

    // We need to determine the place that the widget is being dropped in
    // the target.
    let dragOverItem, dragValue;
    if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) {
      // We'll assume if the user is dragging directly over the target, that
      // they're attempting to append a child to that target.
      dragOverItem =
        (targetAreaType == "toolbar"
          ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
          : targetNode.lastElementChild) || targetNode;
      dragValue = "after";
    } else {
      let targetParent = targetNode.parentNode;
      let position = Array.prototype.indexOf.call(
        targetParent.children,
        targetNode
      );
      if (position == -1) {
        dragOverItem =
          targetAreaType == "toolbar"
            ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
            : targetNode.lastElementChild;
        dragValue = "after";
      } else {
        dragOverItem = targetParent.children[position];
        if (targetAreaType == "toolbar") {
          // Check if the aDraggedItem is hovered past the first half of dragOverItem
          let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
          let dropTargetCenter = itemRect.left + itemRect.width / 2;
          let existingDir = dragOverItem.getAttribute("dragover");
          let dirFactor = this.window.RTL_UI ? -1 : 1;
          if (existingDir == "before") {
            dropTargetCenter +=
              ((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) *
              dirFactor;
          } else {
            dropTargetCenter -=
              ((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) *
              dirFactor;
          }
          let before = this.window.RTL_UI
            ? aEvent.clientX > dropTargetCenter
            : aEvent.clientX < dropTargetCenter;
          dragValue = before ? "before" : "after";
        } else if (targetAreaType == "panel") {
          let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
          let dropTargetCenter = itemRect.top + itemRect.height / 2;
          let existingDir = dragOverItem.getAttribute("dragover");
          if (existingDir == "before") {
            dropTargetCenter +=
              (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2;
          } else {
            dropTargetCenter -=
              (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2;
          }
          dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after";
        } else {
          dragValue = "before";
        }
      }
    }

    if (this._dragOverItem && dragOverItem != this._dragOverItem) {
      this._cancelDragActive(this._dragOverItem, dragOverItem);
    }

    if (
      dragOverItem != this._dragOverItem ||
      dragValue != dragOverItem.getAttribute("dragover")
    ) {
      if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) {
        this._setDragActive(
          dragOverItem,
          dragValue,
          draggedItemId,
--> --------------------

--> maximum size reached

--> --------------------

[ zur Elbe Produktseite wechseln0.69Quellennavigators  Analyse erneut starten  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge