browser/components/customizableui/CustomizableWidgets.jsm
author ffxbld <ffxbld@mozilla.com>
Mon, 18 Nov 2019 10:07:48 +0000
changeset 555707 0c050b533d109b5081b5696a81d0f6e3001d0891
parent 544221 4381f16ec9948a70802d7cc28cd021b094c7ba74
permissions -rw-r--r--
No Bug, mozilla-release repo-update blocklist remote-settings - a=repo-update r=RyanVM Differential Revision: https://phabricator.services.mozilla.com/D53397

/* 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/. */

"use strict";

var EXPORTED_SYMBOLS = ["CustomizableWidgets"];

const { CustomizableUI } = ChromeUtils.import(
  "resource:///modules/CustomizableUI.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);

XPCOMUtils.defineLazyModuleGetters(this, {
  PanelView: "resource:///modules/PanelMultiView.jsm",
  RecentlyClosedTabsAndWindowsMenuUtils:
    "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
  ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
  CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
  Sanitizer: "resource:///modules/Sanitizer.jsm",
  SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
});

XPCOMUtils.defineLazyGetter(this, "CharsetBundle", function() {
  const kCharsetBundle = "chrome://global/locale/charsetMenu.properties";
  return Services.strings.createBundle(kCharsetBundle);
});

const kPrefCustomizationDebug = "browser.uiCustomization.debug";

XPCOMUtils.defineLazyGetter(this, "log", () => {
  let scope = {};
  ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
  let debug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
  let consoleOptions = {
    maxLogLevel: debug ? "all" : "log",
    prefix: "CustomizableWidgets",
  };
  return new scope.ConsoleAPI(consoleOptions);
});

function setAttributes(aNode, aAttrs) {
  let doc = aNode.ownerDocument;
  for (let [name, value] of Object.entries(aAttrs)) {
    if (!value) {
      if (aNode.hasAttribute(name)) {
        aNode.removeAttribute(name);
      }
    } else {
      if (name == "shortcutId") {
        continue;
      }
      if (name == "label" || name == "tooltiptext") {
        let stringId = typeof value == "string" ? value : name;
        let additionalArgs = [];
        if (aAttrs.shortcutId) {
          let shortcut = doc.getElementById(aAttrs.shortcutId);
          if (shortcut) {
            additionalArgs.push(ShortcutUtils.prettifyShortcut(shortcut));
          }
        }
        value = CustomizableUI.getLocalizedProperty(
          { id: aAttrs.id },
          stringId,
          additionalArgs
        );
      }
      aNode.setAttribute(name, value);
    }
  }
}

const CustomizableWidgets = [
  {
    id: "history-panelmenu",
    type: "view",
    viewId: "PanelUI-history",
    shortcutId: "key_gotoHistory",
    tooltiptext: "history-panelmenu.tooltiptext2",
    recentlyClosedTabsPanel: "appMenu-library-recentlyClosedTabs",
    recentlyClosedWindowsPanel: "appMenu-library-recentlyClosedWindows",
    handleEvent(event) {
      switch (event.type) {
        case "PanelMultiViewHidden":
          this.onPanelMultiViewHidden(event);
          break;
        case "ViewShowing":
          this.onSubViewShowing(event);
          break;
        default:
          throw new Error(`Unsupported event for '${this.id}'`);
      }
    },
    onViewShowing(event) {
      if (this._panelMenuView) {
        return;
      }

      let panelview = event.target;
      let document = panelview.ownerDocument;
      let window = document.defaultView;

      // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42.
      let query =
        "place:queryType=" +
        Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY +
        "&sort=" +
        Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING +
        "&maxResults=42&excludeQueries=1";

      this._panelMenuView = new window.PlacesPanelview(
        document.getElementById("appMenu_historyMenu"),
        panelview,
        query
      );
      // When either of these sub-subviews show, populate them with recently closed
      // objects data.
      document
        .getElementById(this.recentlyClosedTabsPanel)
        .addEventListener("ViewShowing", this);
      document
        .getElementById(this.recentlyClosedWindowsPanel)
        .addEventListener("ViewShowing", this);
      // When the popup is hidden (thus the panelmultiview node as well), make
      // sure to stop listening to PlacesDatabase updates.
      panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this);
    },
    onViewHiding(event) {
      log.debug("History view is being hidden!");
    },
    onPanelMultiViewHidden(event) {
      let panelMultiView = event.target;
      let document = panelMultiView.ownerDocument;
      if (this._panelMenuView) {
        this._panelMenuView.uninit();
        delete this._panelMenuView;
        document
          .getElementById(this.recentlyClosedTabsPanel)
          .removeEventListener("ViewShowing", this);
        document
          .getElementById(this.recentlyClosedWindowsPanel)
          .removeEventListener("ViewShowing", this);
      }
      panelMultiView.removeEventListener("PanelMultiViewHidden", this);
    },
    onSubViewShowing(event) {
      let panelview = event.target;
      let document = event.target.ownerDocument;
      let window = document.defaultView;
      let viewType =
        panelview.id == this.recentlyClosedTabsPanel ? "Tabs" : "Windows";

      this._panelMenuView.clearAllContents(panelview);

      let utils = RecentlyClosedTabsAndWindowsMenuUtils;
      let method = `get${viewType}Fragment`;
      let fragment = utils[method](window, "toolbarbutton", true);
      let elementCount = fragment.childElementCount;
      this._panelMenuView._setEmptyPopupStatus(panelview, !elementCount);
      if (!elementCount) {
        return;
      }

      let body = document.createXULElement("vbox");
      body.className = "panel-subview-body";
      body.appendChild(fragment);
      let footer;
      while (--elementCount >= 0) {
        let element = body.children[elementCount];
        CustomizableUI.addShortcut(element);
        element.classList.add("subviewbutton");
        if (element.classList.contains("restoreallitem")) {
          footer = element;
          element.classList.add("panel-subview-footer");
        } else {
          element.classList.add("subviewbutton-iconic", "bookmark-item");
        }
      }
      panelview.appendChild(body);
      panelview.appendChild(footer);
    },
  },
  {
    id: "save-page-button",
    shortcutId: "key_savePage",
    tooltiptext: "save-page-button.tooltiptext3",
    onCommand(aEvent) {
      let win = aEvent.target.ownerGlobal;
      win.saveBrowser(win.gBrowser.selectedBrowser);
    },
  },
  {
    id: "find-button",
    shortcutId: "key_find",
    tooltiptext: "find-button.tooltiptext3",
    onCommand(aEvent) {
      let win = aEvent.target.ownerGlobal;
      if (win.gLazyFindCommand) {
        win.gLazyFindCommand("onFindCommand");
      }
    },
  },
  {
    id: "open-file-button",
    shortcutId: "openFileKb",
    tooltiptext: "open-file-button.tooltiptext3",
    onCommand(aEvent) {
      let win = aEvent.target.ownerGlobal;
      win.BrowserOpenFileWindow();
    },
  },
  {
    id: "sidebar-button",
    tooltiptext: "sidebar-button.tooltiptext2",
    onCommand(aEvent) {
      let win = aEvent.target.ownerGlobal;
      win.SidebarUI.toggle();
    },
    onCreated(aNode) {
      // Add an observer so the button is checked while the sidebar is open
      let doc = aNode.ownerDocument;
      let obChecked = doc.createXULElement("observes");
      obChecked.setAttribute("element", "sidebar-box");
      obChecked.setAttribute("attribute", "checked");
      let obPosition = doc.createXULElement("observes");
      obPosition.setAttribute("element", "sidebar-box");
      obPosition.setAttribute("attribute", "positionend");

      aNode.appendChild(obChecked);
      aNode.appendChild(obPosition);
    },
  },
  {
    id: "add-ons-button",
    shortcutId: "key_openAddons",
    tooltiptext: "add-ons-button.tooltiptext3",
    onCommand(aEvent) {
      let win = aEvent.target.ownerGlobal;
      win.BrowserOpenAddonsMgr();
    },
  },
  {
    id: "zoom-controls",
    type: "custom",
    tooltiptext: "zoom-controls.tooltiptext2",
    onBuild(aDocument) {
      let buttons = [
        {
          id: "zoom-out-button",
          command: "cmd_fullZoomReduce",
          label: true,
          closemenu: "none",
          tooltiptext: "tooltiptext2",
          shortcutId: "key_fullZoomReduce",
          class: "toolbarbutton-1 toolbarbutton-combined",
        },
        {
          id: "zoom-reset-button",
          command: "cmd_fullZoomReset",
          closemenu: "none",
          tooltiptext: "tooltiptext2",
          shortcutId: "key_fullZoomReset",
          class: "toolbarbutton-1 toolbarbutton-combined",
        },
        {
          id: "zoom-in-button",
          command: "cmd_fullZoomEnlarge",
          closemenu: "none",
          label: true,
          tooltiptext: "tooltiptext2",
          shortcutId: "key_fullZoomEnlarge",
          class: "toolbarbutton-1 toolbarbutton-combined",
        },
      ];

      let node = aDocument.createXULElement("toolbaritem");
      node.setAttribute("id", "zoom-controls");
      node.setAttribute(
        "label",
        CustomizableUI.getLocalizedProperty(this, "label")
      );
      node.setAttribute(
        "title",
        CustomizableUI.getLocalizedProperty(this, "tooltiptext")
      );
      // Set this as an attribute in addition to the property to make sure we can style correctly.
      node.setAttribute("removable", "true");
      node.classList.add("chromeclass-toolbar-additional");
      node.classList.add("toolbaritem-combined-buttons");

      buttons.forEach(function(aButton, aIndex) {
        if (aIndex != 0) {
          node.appendChild(aDocument.createXULElement("separator"));
        }
        let btnNode = aDocument.createXULElement("toolbarbutton");
        setAttributes(btnNode, aButton);
        node.appendChild(btnNode);
      });
      return node;
    },
  },
  {
    id: "edit-controls",
    type: "custom",
    tooltiptext: "edit-controls.tooltiptext2",
    onBuild(aDocument) {
      let buttons = [
        {
          id: "cut-button",
          command: "cmd_cut",
          label: true,
          tooltiptext: "tooltiptext2",
          shortcutId: "key_cut",
          class: "toolbarbutton-1 toolbarbutton-combined",
        },
        {
          id: "copy-button",
          command: "cmd_copy",
          label: true,
          tooltiptext: "tooltiptext2",
          shortcutId: "key_copy",
          class: "toolbarbutton-1 toolbarbutton-combined",
        },
        {
          id: "paste-button",
          command: "cmd_paste",
          label: true,
          tooltiptext: "tooltiptext2",
          shortcutId: "key_paste",
          class: "toolbarbutton-1 toolbarbutton-combined",
        },
      ];

      let node = aDocument.createXULElement("toolbaritem");
      node.setAttribute("id", "edit-controls");
      node.setAttribute(
        "label",
        CustomizableUI.getLocalizedProperty(this, "label")
      );
      node.setAttribute(
        "title",
        CustomizableUI.getLocalizedProperty(this, "tooltiptext")
      );
      // Set this as an attribute in addition to the property to make sure we can style correctly.
      node.setAttribute("removable", "true");
      node.classList.add("chromeclass-toolbar-additional");
      node.classList.add("toolbaritem-combined-buttons");

      buttons.forEach(function(aButton, aIndex) {
        if (aIndex != 0) {
          node.appendChild(aDocument.createXULElement("separator"));
        }
        let btnNode = aDocument.createXULElement("toolbarbutton");
        setAttributes(btnNode, aButton);
        node.appendChild(btnNode);
      });

      let listener = {
        onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
          if (aWidgetId != this.id || aDoc != aDocument) {
            return;
          }
          CustomizableUI.removeListener(listener);
        },
        onWidgetOverflow(aWidgetNode) {
          if (aWidgetNode == node) {
            node.ownerGlobal.updateEditUIVisibility();
          }
        },
        onWidgetUnderflow(aWidgetNode) {
          if (aWidgetNode == node) {
            node.ownerGlobal.updateEditUIVisibility();
          }
        },
      };
      CustomizableUI.addListener(listener);

      return node;
    },
  },
  {
    id: "characterencoding-button",
    label: "characterencoding-button2.label",
    type: "view",
    viewId: "PanelUI-characterEncodingView",
    tooltiptext: "characterencoding-button2.tooltiptext",
    maybeDisableMenu(aDocument) {
      let window = aDocument.defaultView;
      return !(
        window.gBrowser &&
        window.gBrowser.selectedBrowser.mayEnableCharacterEncodingMenu
      );
    },
    populateList(aDocument, aContainerId, aSection) {
      let containerElem = aDocument.getElementById(aContainerId);

      containerElem.addEventListener("command", this.onCommand);

      let list = this.charsetInfo[aSection];

      for (let item of list) {
        let elem = aDocument.createXULElement("toolbarbutton");
        elem.setAttribute("label", item.label);
        elem.setAttribute("type", "checkbox");
        elem.section = aSection;
        elem.value = item.value;
        elem.setAttribute("class", "subviewbutton");
        containerElem.appendChild(elem);
      }
    },
    updateCurrentCharset(aDocument) {
      let currentCharset =
        aDocument.defaultView.gBrowser.selectedBrowser.characterSet;
      let {
        charsetAutodetected,
      } = aDocument.defaultView.gBrowser.selectedBrowser;
      currentCharset = CharsetMenu.foldCharset(
        currentCharset,
        charsetAutodetected
      );

      let pinnedContainer = aDocument.getElementById(
        "PanelUI-characterEncodingView-pinned"
      );
      let charsetContainer = aDocument.getElementById(
        "PanelUI-characterEncodingView-charsets"
      );
      let elements = [
        ...pinnedContainer.children,
        ...charsetContainer.children,
      ];

      this._updateElements(elements, currentCharset);
    },
    updateCurrentDetector(aDocument) {
      let detectorContainer = aDocument.getElementById(
        "PanelUI-characterEncodingView-autodetect"
      );
      let currentDetector;
      try {
        currentDetector = Services.prefs.getComplexValue(
          "intl.charset.detector",
          Ci.nsIPrefLocalizedString
        ).data;
      } catch (e) {}

      this._updateElements(detectorContainer.children, currentDetector);
    },
    _updateElements(aElements, aCurrentItem) {
      if (!aElements.length) {
        return;
      }
      let disabled = this.maybeDisableMenu(aElements[0].ownerDocument);
      for (let elem of aElements) {
        if (disabled) {
          elem.setAttribute("disabled", "true");
        } else {
          elem.removeAttribute("disabled");
        }
        if (elem.value.toLowerCase() == aCurrentItem.toLowerCase()) {
          elem.setAttribute("checked", "true");
        } else {
          elem.removeAttribute("checked");
        }
      }
    },
    onViewShowing(aEvent) {
      if (!this._inited) {
        this.onInit();
      }
      let document = aEvent.target.ownerDocument;

      let autoDetectLabelId = "PanelUI-characterEncodingView-autodetect-label";
      let autoDetectLabel = document.getElementById(autoDetectLabelId);
      if (!autoDetectLabel.hasAttribute("value")) {
        let label = CharsetBundle.GetStringFromName("charsetMenuAutodet");
        autoDetectLabel.setAttribute("value", label);
        this.populateList(
          document,
          "PanelUI-characterEncodingView-pinned",
          "pinnedCharsets"
        );
        this.populateList(
          document,
          "PanelUI-characterEncodingView-charsets",
          "otherCharsets"
        );
        this.populateList(
          document,
          "PanelUI-characterEncodingView-autodetect",
          "detectors"
        );
      }
      this.updateCurrentDetector(document);
      this.updateCurrentCharset(document);
    },
    onCommand(aEvent) {
      let node = aEvent.target;
      if (!node.hasAttribute || !node.section) {
        return;
      }

      let window = node.ownerGlobal;
      let section = node.section;
      let value = node.value;

      // The behavior as implemented here is directly based off of the
      // `MultiplexHandler()` method in browser.js.
      if (section != "detectors") {
        window.BrowserSetForcedCharacterSet(value);
      } else {
        // Set the detector pref.
        try {
          Services.prefs.setStringPref("intl.charset.detector", value);
        } catch (e) {
          Cu.reportError("Failed to set the intl.charset.detector preference.");
        }
        // Prepare a browser page reload with a changed charset.
        window.BrowserCharsetReload();
      }
    },
    onCreated(aNode) {
      let document = aNode.ownerDocument;

      let updateButton = () => {
        if (this.maybeDisableMenu(document)) {
          aNode.setAttribute("disabled", "true");
        } else {
          aNode.removeAttribute("disabled");
        }
      };

      let getPanel = () => {
        let { PanelUI } = document.ownerGlobal;
        return PanelUI.overflowPanel;
      };

      if (
        CustomizableUI.getAreaType(this.currentArea) ==
        CustomizableUI.TYPE_MENU_PANEL
      ) {
        getPanel().addEventListener("popupshowing", updateButton);
      }

      let listener = {
        onWidgetAdded: (aWidgetId, aArea) => {
          if (aWidgetId != this.id) {
            return;
          }
          if (
            CustomizableUI.getAreaType(aArea) == CustomizableUI.TYPE_MENU_PANEL
          ) {
            getPanel().addEventListener("popupshowing", updateButton);
          }
        },
        onWidgetRemoved: (aWidgetId, aPrevArea) => {
          if (aWidgetId != this.id) {
            return;
          }
          aNode.removeAttribute("disabled");
          if (
            CustomizableUI.getAreaType(aPrevArea) ==
            CustomizableUI.TYPE_MENU_PANEL
          ) {
            getPanel().removeEventListener("popupshowing", updateButton);
          }
        },
        onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
          if (aWidgetId != this.id || aDoc != document) {
            return;
          }

          CustomizableUI.removeListener(listener);
          getPanel().removeEventListener("popupshowing", updateButton);
        },
      };
      CustomizableUI.addListener(listener);
      this.onInit();
    },
    onInit() {
      this._inited = true;
      if (!this.charsetInfo) {
        this.charsetInfo = CharsetMenu.getData();
      }
    },
  },
  {
    id: "email-link-button",
    tooltiptext: "email-link-button.tooltiptext3",
    onCommand(aEvent) {
      let win = aEvent.view;
      win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser);
    },
  },
];

if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
  CustomizableWidgets.push({
    id: "sync-button",
    label: "remotetabs-panelmenu.label",
    tooltiptext: "remotetabs-panelmenu.tooltiptext2",
    type: "view",
    viewId: "PanelUI-remotetabs",
    deckIndices: {
      DECKINDEX_TABS: 0,
      DECKINDEX_TABSDISABLED: 1,
      DECKINDEX_FETCHING: 2,
      DECKINDEX_NOCLIENTS: 3,
    },
    TABS_PER_PAGE: 25,
    NEXT_PAGE_MIN_TABS: 5, // Minimum number of tabs displayed when we click "Show All"
    onViewShowing(aEvent) {
      let doc = aEvent.target.ownerDocument;
      this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist");
      Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);

      if (SyncedTabs.isConfiguredToSyncTabs) {
        if (SyncedTabs.hasSyncedThisSession) {
          this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
        } else {
          // Sync hasn't synced tabs yet, so show the "fetching" panel.
          this.setDeckIndex(this.deckIndices.DECKINDEX_FETCHING);
        }
        // force a background sync.
        SyncedTabs.syncTabs().catch(ex => {
          Cu.reportError(ex);
        });
        // show the current list - it will be updated by our observer.
        this._showTabs();
      } else {
        // not configured to sync tabs, so no point updating the list.
        this.setDeckIndex(this.deckIndices.DECKINDEX_TABSDISABLED);
      }
    },
    onViewHiding() {
      Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
      this._tabsList = null;
    },
    _tabsList: null,
    observe(subject, topic, data) {
      switch (topic) {
        case SyncedTabs.TOPIC_TABS_CHANGED:
          this._showTabs();
          break;
        default:
          break;
      }
    },
    setDeckIndex(index) {
      let deck = this._tabsList.ownerDocument.getElementById(
        "PanelUI-remotetabs-deck"
      );
      // We call setAttribute instead of relying on the XBL property setter due
      // to things going wrong when we try and set the index before the XBL
      // binding has been created - see bug 1241851 for the gory details.
      deck.setAttribute("selectedIndex", index);
    },

    _showTabsPromise: Promise.resolve(),
    // Update the tab list after any existing in-flight updates are complete.
    _showTabs(paginationInfo) {
      this._showTabsPromise = this._showTabsPromise.then(
        () => {
          return this.__showTabs(paginationInfo);
        },
        e => {
          Cu.reportError(e);
        }
      );
    },
    // Return a new promise to update the tab list.
    __showTabs(paginationInfo) {
      if (!this._tabsList) {
        // Closed between the previous `this._showTabsPromise`
        // resolving and now.
        return undefined;
      }
      let doc = this._tabsList.ownerDocument;
      return SyncedTabs.getTabClients()
        .then(clients => {
          // The view may have been hidden while the promise was resolving.
          if (!this._tabsList) {
            return;
          }
          if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
            // the "fetching tabs" deck is being shown - let's leave it there.
            // When that first sync completes we'll be notified and update.
            return;
          }

          if (clients.length === 0) {
            this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS);
            return;
          }

          this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
          this._clearTabList();
          SyncedTabs.sortTabClientsByLastUsed(clients);
          let fragment = doc.createDocumentFragment();

          for (let client of clients) {
            // add a menu separator for all clients other than the first.
            if (fragment.lastElementChild) {
              let separator = doc.createXULElement("menuseparator");
              fragment.appendChild(separator);
            }
            if (paginationInfo && paginationInfo.clientId == client.id) {
              this._appendClient(client, fragment, paginationInfo.maxTabs);
            } else {
              this._appendClient(client, fragment);
            }
          }
          this._tabsList.appendChild(fragment);
          PanelView.forNode(
            this._tabsList.closest("panelview")
          ).descriptionHeightWorkaround();
        })
        .catch(err => {
          Cu.reportError(err);
        })
        .then(() => {
          // an observer for tests.
          Services.obs.notifyObservers(
            null,
            "synced-tabs-menu:test:tabs-updated"
          );
        });
    },
    _clearTabList() {
      let list = this._tabsList;
      while (list.lastChild) {
        list.lastChild.remove();
      }
    },
    _showNoClientMessage() {
      this._appendMessageLabel("notabslabel");
    },
    _appendMessageLabel(messageAttr, appendTo = null) {
      if (!appendTo) {
        appendTo = this._tabsList;
      }
      let message = this._tabsList.getAttribute(messageAttr);
      let doc = this._tabsList.ownerDocument;
      let messageLabel = doc.createXULElement("label");
      messageLabel.textContent = message;
      appendTo.appendChild(messageLabel);
      return messageLabel;
    },
    _appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
      let doc = attachFragment.ownerDocument;
      // Create the element for the remote client.
      let clientItem = doc.createXULElement("label");
      clientItem.setAttribute("itemtype", "client");
      let window = doc.defaultView;
      clientItem.setAttribute(
        "tooltiptext",
        window.gSync.formatLastSyncDate(new Date(client.lastModified))
      );
      clientItem.textContent = client.name;

      attachFragment.appendChild(clientItem);

      if (client.tabs.length == 0) {
        let label = this._appendMessageLabel(
          "notabsforclientlabel",
          attachFragment
        );
        label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
      } else {
        // If this page will display all tabs, show no additional buttons.
        // If the next page will display all the remaining tabs, show a "Show All" button
        // Otherwise, show a "Shore More" button
        let hasNextPage = client.tabs.length > maxTabs;
        let nextPageIsLastPage =
          hasNextPage && maxTabs + this.TABS_PER_PAGE >= client.tabs.length;
        if (nextPageIsLastPage) {
          // When the user clicks "Show All", try to have at least NEXT_PAGE_MIN_TABS more tabs
          // to display in order to avoid user frustration
          maxTabs = Math.min(
            client.tabs.length - this.NEXT_PAGE_MIN_TABS,
            maxTabs
          );
        }
        if (hasNextPage) {
          client.tabs = client.tabs.slice(0, maxTabs);
        }
        for (let tab of client.tabs) {
          let tabEnt = this._createTabElement(doc, tab);
          attachFragment.appendChild(tabEnt);
        }
        if (hasNextPage) {
          let showAllEnt = this._createShowMoreElement(
            doc,
            client.id,
            nextPageIsLastPage ? Infinity : maxTabs + this.TABS_PER_PAGE
          );
          attachFragment.appendChild(showAllEnt);
        }
      }
    },
    _createTabElement(doc, tabInfo) {
      let item = doc.createXULElement("toolbarbutton");
      let tooltipText =
        (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
      item.setAttribute("itemtype", "tab");
      item.setAttribute("class", "subviewbutton");
      item.setAttribute("targetURI", tabInfo.url);
      item.setAttribute(
        "label",
        tabInfo.title != "" ? tabInfo.title : tabInfo.url
      );
      item.setAttribute("image", tabInfo.icon);
      item.setAttribute("tooltiptext", tooltipText);
      // We need to use "click" instead of "command" here so openUILink
      // respects different buttons (eg, to open in a new tab).
      item.addEventListener("click", e => {
        doc.defaultView.openUILink(tabInfo.url, e, {
          triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
            {}
          ),
        });
        if (doc.defaultView.whereToOpenLink(e) != "current") {
          e.preventDefault();
          e.stopPropagation();
        } else {
          CustomizableUI.hidePanelForNode(item);
        }
      });
      return item;
    },
    _createShowMoreElement(doc, clientId, showCount) {
      let labelAttr, tooltipAttr;
      if (showCount === Infinity) {
        labelAttr = "showAllLabel";
        tooltipAttr = "showAllTooltipText";
      } else {
        labelAttr = "showMoreLabel";
        tooltipAttr = "showMoreTooltipText";
      }
      let showAllItem = doc.createXULElement("toolbarbutton");
      showAllItem.setAttribute("itemtype", "showmorebutton");
      showAllItem.setAttribute("class", "subviewbutton");
      let label = this._tabsList.getAttribute(labelAttr);
      showAllItem.setAttribute("label", label);
      let tooltipText = this._tabsList.getAttribute(tooltipAttr);
      showAllItem.setAttribute("tooltiptext", tooltipText);
      showAllItem.addEventListener("click", e => {
        e.preventDefault();
        e.stopPropagation();
        this._showTabs({ clientId, maxTabs: showCount });
      });
      return showAllItem;
    },
  });
}

let preferencesButton = {
  id: "preferences-button",
  onCommand(aEvent) {
    let win = aEvent.target.ownerGlobal;
    win.openPreferences(undefined);
  },
};
if (AppConstants.platform == "win") {
  preferencesButton.label = "preferences-button.labelWin";
  preferencesButton.tooltiptext = "preferences-button.tooltipWin2";
} else if (AppConstants.platform == "macosx") {
  preferencesButton.tooltiptext = "preferences-button.tooltiptext.withshortcut";
  preferencesButton.shortcutId = "key_preferencesCmdMac";
} else {
  preferencesButton.tooltiptext = "preferences-button.tooltiptext2";
}
CustomizableWidgets.push(preferencesButton);

if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) {
  CustomizableWidgets.push({
    id: "panic-button",
    type: "view",
    viewId: "PanelUI-panicView",

    forgetButtonCalled(aEvent) {
      let doc = aEvent.target.ownerDocument;
      let group = doc.getElementById("PanelUI-panic-timeSpan");
      let itemsToClear = [
        "cookies",
        "history",
        "openWindows",
        "formdata",
        "sessions",
        "cache",
        "downloads",
        "offlineApps",
      ];
      let newWindowPrivateState = PrivateBrowsingUtils.isWindowPrivate(
        doc.defaultView
      )
        ? "private"
        : "non-private";
      let promise = Sanitizer.sanitize(itemsToClear, {
        ignoreTimespan: false,
        range: Sanitizer.getClearRange(+group.value),
        privateStateForNewWindow: newWindowPrivateState,
      });
      promise.then(function() {
        let otherWindow = Services.wm.getMostRecentWindow("navigator:browser");
        if (otherWindow.closed) {
          Cu.reportError("Got a closed window!");
        }
        if (otherWindow.PanicButtonNotifier) {
          otherWindow.PanicButtonNotifier.notify();
        } else {
          otherWindow.PanicButtonNotifierShouldNotify = true;
        }
      });
    },
    handleEvent(aEvent) {
      switch (aEvent.type) {
        case "command":
          this.forgetButtonCalled(aEvent);
          break;
      }
    },
    onViewShowing(aEvent) {
      let win = aEvent.target.ownerGlobal;
      let doc = win.document;
      let eventBlocker = null;
      if (!doc.querySelector("#PanelUI-panic-timeframe")) {
        win.MozXULElement.insertFTLIfNeeded("browser/panicButton.ftl");
        let frag = win.MozXULElement.parseXULToFragment(`
          <vbox class="panel-subview-body">
            <hbox id="PanelUI-panic-timeframe">
              <image id="PanelUI-panic-timeframe-icon" alt=""/>
              <vbox flex="1">
                <description data-l10n-id="panic-main-timeframe-desc" id="PanelUI-panic-mainDesc"></description>
                <radiogroup id="PanelUI-panic-timeSpan" aria-labelledby="PanelUI-panic-mainDesc" closemenu="none">
                  <radio id="PanelUI-panic-5min" data-l10n-id="panic-button-5min" selected="true"
                        value="5" class="subviewradio"/>
                  <radio id="PanelUI-panic-2hr" data-l10n-id="panic-button-2hr"
                        value="2" class="subviewradio"/>
                  <radio id="PanelUI-panic-day" data-l10n-id="panic-button-day"
                        value="6" class="subviewradio"/>
                </radiogroup>
              </vbox>
            </hbox>
            <vbox id="PanelUI-panic-explanations">
              <label id="PanelUI-panic-actionlist-main-label" data-l10n-id="panic-button-action-desc"></label>

              <label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-tabs-and-windows"></label>
              <label id="PanelUI-panic-actionlist-cookies" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-cookies"></label>
              <label id="PanelUI-panic-actionlist-history" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-history"></label>
              <label id="PanelUI-panic-actionlist-newwindow" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-open-new-window"></label>

              <label id="PanelUI-panic-warning" data-l10n-id="panic-button-undo-warning"></label>
            </vbox>
            <button id="PanelUI-panic-view-button"
                    data-l10n-id="panic-button-forget-button"/>
          </vbox>
        `);

        aEvent.target.appendChild(frag);
        eventBlocker = doc.l10n.translateElements([aEvent.target]);
      }

      let forgetButton = aEvent.target.querySelector(
        "#PanelUI-panic-view-button"
      );
      let group = doc.getElementById("PanelUI-panic-timeSpan");
      group.selectedItem = doc.getElementById("PanelUI-panic-5min");
      forgetButton.addEventListener("command", this);

      if (eventBlocker) {
        aEvent.detail.addBlocker(eventBlocker);
      }
    },
    onViewHiding(aEvent) {
      let forgetButton = aEvent.target.querySelector(
        "#PanelUI-panic-view-button"
      );
      forgetButton.removeEventListener("command", this);
    },
  });
}

if (PrivateBrowsingUtils.enabled) {
  CustomizableWidgets.push({
    id: "privatebrowsing-button",
    shortcutId: "key_privatebrowsing",
    onCommand(e) {
      let win = e.target.ownerGlobal;
      win.OpenBrowserWindow({ private: true });
    },
  });
}