Source: customizableUI.js

/*
 * This file is part of Adblock Plus <https://adblockplus.org/>,
 * Copyright (C) 2006-present eyeo GmbH
 *
 * Adblock Plus is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * Adblock Plus is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Adblock Plus.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @fileOverview This emulates a subset of the CustomizableUI API from Firefox 28.
 */

let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", null);

let {Utils} = require("utils");

// UI module has to be referenced lazily to avoid circular references
XPCOMUtils.defineLazyGetter(this, "UI", () => require("ui").UI);

let widgets = new Map();

function getToolbox(/**Window*/ window, /**Widget*/ widget) /**Element*/
{
  if (!("defaultArea" in widget) || !widget.defaultArea)
    return null;

  let toolbar = UI.findElement(window, widget.defaultArea);
  if (!toolbar)
    return null;

  let toolbox = toolbar.toolbox;
  if (toolbox && ("palette" in toolbox) && toolbox.palette)
    return toolbox;
  else
    return null;
}

function getToolbar(/**Element*/ element) /**Element*/
{
  for (let parent = element.parentNode; parent; parent = parent.parentNode)
    if (parent.localName == "toolbar")
      return parent;
  return null;
}

function getPaletteItem(/**Element*/ toolbox, /**String*/ id) /**Element*/
{
  for (let child of toolbox.palette.children)
    if (child.id == id)
      return child;

  return null;
}

function restoreWidget(/**Element*/ toolbox, /**Widget*/ widget)
{
  // Create node
  let node = widget.onBuild(toolbox.ownerDocument);

  // Insert into the palette first
  toolbox.palette.insertBefore(node, toolbox.palette.firstChild);

  // Now find out where we should put it
  let position = toolbox.getAttribute(widget.positionAttribute);
  if (!/^\S*,\S*,\S*$/.test(position))
    position = null;

  if (position == null)
  {
    // No explicitly saved position but maybe we can find it in a currentset
    // attribute somewhere.
    let toolbars = toolbox.externalToolbars.slice();
    for (let child of toolbox.children)
      if (child.localName == "toolbar")
        toolbars.push(child);
    for (let toolbar of toolbars)
    {
      let currentSet = toolbar.getAttribute("currentset");
      if (currentSet)
      {
        let items = currentSet.split(",");
        let index = items.indexOf(widget.id);
        if (index >= 0)
        {
          let before = (index + 1 < items.length ? items[index + 1] : "");
          position = "visible," + toolbar.id + "," + before;
          toolbox.setAttribute(widget.positionAttribute, position);
          toolbox.ownerDocument.persist(toolbox.id, widget.positionAttribute);
          break;
        }
      }
    }
  }

  showWidget(toolbox, widget, position);
}

function showWidget(/**Element*/ toolbox, /**Widget*/ widget, /**String*/ position)
{
  let visible = "visible", parent = null, before = null;
  if (position)
  {
    [visible, parent, before] = position.split(",", 3);
    parent = toolbox.ownerDocument.getElementById(parent);
    if (before == "")
      before = null;
    else
      before = toolbox.ownerDocument.getElementById(before);
    if (before && before.parentNode != parent)
      before = null;
  }

  if (visible == "visible" && !parent)
  {
    let insertionPoint = {
      parent: widget.defaultArea
    };
    if (typeof widget.defaultBefore != "undefined")
      insertionPoint.before = widget.defaultBefore;
    if (typeof widget.defaultAfter != "undefined")
      insertionPoint.after = widget.defaultAfter;

    [parent, before] = UI.resolveInsertionPoint(toolbox.ownerDocument.defaultView, insertionPoint);
  }

  if (parent && parent.localName != "toolbar")
    parent = null;

  if (visible != "visible")
  {
    // Move to palette if the item is currently visible
    let node = toolbox.ownerDocument.getElementById(widget.id);
    if (node)
      toolbox.palette.appendChild(node);
  }
  else if (parent)
  {
    // Add the item to the toolbar
    let items = parent.currentSet.split(",");
    let index = (before ? items.indexOf(before.id) : -1);
    if (index < 0)
      before = null;
    parent.insertItem(widget.id, before, null, false);
  }

  saveState(toolbox, widget);
}

function removeWidget(/**Window*/ window, /**Widget*/ widget)
{
  let element = window.document.getElementById(widget.id);
  if (element)
    element.parentNode.removeChild(element);

  let toolbox = getToolbox(window, widget);
  if (toolbox)
  {
    let paletteItem = getPaletteItem(toolbox, widget.id);
    if (paletteItem)
      paletteItem.parentNode.removeChild(paletteItem);
  }
}

function onToolbarCustomization(/**Event*/ event)
{
  let toolbox = event.currentTarget;
  for (let [id, widget] of widgets)
    saveState(toolbox, widget);
}

function saveState(/**Element*/ toolbox, /**Widget*/ widget)
{
  let node = toolbox.ownerDocument.getElementById(widget.id);

  let position = toolbox.getAttribute(widget.positionAttribute) || "hidden,,";
  if (node && node.parentNode.localName != "toolbarpalette")
  {
    if (typeof widget.onAdded == "function")
      widget.onAdded(node)

    let toolbar = getToolbar(node);
    position = "visible," + toolbar.id + "," + (node.nextSibling ? node.nextSibling.id : "");
  }
  else
    position = position.replace(/^visible,/, "hidden,")

  toolbox.setAttribute(widget.positionAttribute, position);
  toolbox.ownerDocument.persist(toolbox.id, widget.positionAttribute);
}

let CustomizableUI = exports.CustomizableUI =
{
  createWidget: function(widget)
  {
    if (typeof widget.id == "undefined" ||
        typeof widget.defaultArea == "undefined" ||
        typeof widget.positionAttribute == "undefined")
    {
      throw new Error("Unexpected: required property missing from the widget data");
    }
    widgets.set(widget.id, widget);

    // Show widget in any existing windows
    for (let window of UI.applicationWindows)
    {
      let toolbox = getToolbox(window, widget);
      if (toolbox)
      {
        toolbox.addEventListener("aftercustomization", onToolbarCustomization, false);
        restoreWidget(toolbox, widget);
      }
    }
  },

  destroyWidget: function(id)
  {
    // Don't do anything here. This function is called on shutdown,
    // removeFromWindow will take care of cleaning up already.
  },

  getPlacementOfWidget: function(id)
  {
    let window = UI.currentWindow;
    if (!window)
      return null;

    let widget = window.document.getElementById(id);
    if (!widget)
      return null;

    let toolbar = getToolbar(widget);
    if (!toolbar)
      return null;

    return {area: toolbar.id};
  },

  addWidgetToArea: function(id)
  {
    // Note: the official API function also has area and position parameters.
    // We ignore those here and simply restore the previous position instead.
    let widget = widgets.get(id);
    for (let window of UI.applicationWindows)
    {
      let toolbox = getToolbox(window, widget);
      if (!toolbox)
        continue;

      let position = toolbox.getAttribute(widget.positionAttribute);
      if (position)
        position = position.replace(/^hidden,/, "visible,");
      showWidget(toolbox, widget, position);
    }
  },

  removeWidgetFromArea: function(id)
  {
    let widget = widgets.get(id);
    for (let window of UI.applicationWindows)
    {
      let toolbox = getToolbox(window, widget);
      if (!toolbox)
        continue;

      let position = toolbox.getAttribute(widget.positionAttribute);
      if (position)
        position = position.replace(/^visible,/, "hidden,");
      else
        position = "hidden,,";
      showWidget(toolbox, widget, position);
    }
  }
};

let {WindowObserver} = require("windowObserver");
new WindowObserver({
  applyToWindow: function(window)
  {
    let {isKnownWindow} = require("appSupport");
    if (!isKnownWindow(window))
      return;

    for (let [id, widget] of widgets)
    {
      let toolbox = getToolbox(window, widget);
      if (toolbox)
      {
        toolbox.addEventListener("aftercustomization", onToolbarCustomization, false);

        // Restore widget asynchronously to allow the stylesheet to load
        Utils.runAsync(restoreWidget.bind(null, toolbox, widget));
      }
    }
  },

  removeFromWindow: function(window)
  {
    let {isKnownWindow} = require("appSupport");
    if (!isKnownWindow(window))
      return;

    for (let [id, widget] of widgets)
    {
      let toolbox = getToolbox(window, widget);
      if (toolbox)
        toolbox.removeEventListener("aftercustomization", onToolbarCustomization, false);

      removeWidget(window, widget);
    }
  }
});