<?xml version="1.0"?>
<!-- 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/. -->

<bindings id="browserToolbarBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="toolbar" role="xul:toolbar">
    <resources>
      <stylesheet src="chrome://global/skin/toolbar.css"/>
    </resources>
    <implementation>
      <field name="overflowedDuringConstruction">null</field>

      <constructor><![CDATA[
          let scope = {};
          Cu.import("resource:///modules/CustomizableUI.jsm", scope);
          // Add an early overflow event listener that will mark if the
          // toolbar overflowed during construction.
          if (scope.CustomizableUI.isAreaOverflowable(this.id)) {
            this.addEventListener("overflow", this);
            this.addEventListener("underflow", this);
          }

          if (document.readyState == "complete") {
            this._init();
          } else {
            // Need to wait until XUL overlays are loaded. See bug 554279.
            let self = this;
            document.addEventListener("readystatechange", function onReadyStateChange() {
              if (document.readyState != "complete")
                return;
              document.removeEventListener("readystatechange", onReadyStateChange, false);
              self._init();
            }, false);
          }
      ]]></constructor>

      <method name="_init">
        <body><![CDATA[
          let scope = {};
          Cu.import("resource:///modules/CustomizableUI.jsm", scope);
          let CustomizableUI = scope.CustomizableUI;

          // Bug 989289: Forcibly set the now unsupported "mode" and "iconsize"
          // attributes, just in case they accidentally get restored from
          // persistence from a user that's been upgrading and downgrading.
          if (CustomizableUI.isBuiltinToolbar(this.id)) {
            const kAttributes = new Map([["mode", "icons"], ["iconsize", "small"]]);
            for (let [attribute, value] of kAttributes) {
              if (this.getAttribute(attribute) != value) {
                this.setAttribute(attribute, value);
                document.persist(this.id, attribute);
              }
              if (this.toolbox) {
                if (this.toolbox.getAttribute(attribute) != value) {
                  this.toolbox.setAttribute(attribute, value);
                  document.persist(this.toolbox.id, attribute);
                }
              }
            }
          }

          // Searching for the toolbox palette in the toolbar binding because
          // toolbars are constructed first.
          let toolbox = this.toolbox;
          if (toolbox && !toolbox.palette) {
            for (let node of toolbox.children) {
              if (node.localName == "toolbarpalette") {
                // Hold on to the palette but remove it from the document.
                toolbox.palette = node;
                toolbox.removeChild(node);
                break;
              }
            }
          }

          // pass the current set of children for comparison with placements:
          let children = Array.from(this.childNodes)
                              .filter(node => node.getAttribute("skipintoolbarset") != "true" && node.id)
                              .map(node => node.id);
          CustomizableUI.registerToolbarNode(this, children);
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "overflow" && aEvent.detail > 0) {
            if (this.overflowable && this.overflowable.initialized) {
              this.overflowable.onOverflow(aEvent);
            } else {
              this.overflowedDuringConstruction = aEvent;
            }
          } else if (aEvent.type == "underflow" && aEvent.detail > 0) {
            this.overflowedDuringConstruction = null;
          }
        ]]></body>
      </method>

      <method name="insertItem">
        <parameter name="aId"/>
        <parameter name="aBeforeElt"/>
        <parameter name="aWrapper"/>
        <body><![CDATA[
          if (aWrapper) {
            Cu.reportError("Can't insert " + aId + ": using insertItem " +
                           "no longer supports wrapper elements.");
            return null;
          }

          // Hack, the customizable UI code makes this be the last position
          let pos = null;
          if (aBeforeElt) {
            let beforeInfo = CustomizableUI.getPlacementOfWidget(aBeforeElt.id);
            if (beforeInfo.area != this.id) {
              Cu.reportError("Can't insert " + aId + " before " +
                             aBeforeElt.id + " which isn't in this area (" +
                             this.id + ").");
              return null;
            }
            pos = beforeInfo.position;
          }

          CustomizableUI.addWidgetToArea(aId, this.id, pos);
          return this.ownerDocument.getElementById(aId);
        ]]></body>
      </method>

      <property name="toolbarName"
                onget="return this.getAttribute('toolbarname');"
                onset="this.setAttribute('toolbarname', val); return val;"/>

      <property name="customizationTarget" readonly="true">
        <getter><![CDATA[
          if (this._customizationTarget)
            return this._customizationTarget;

          let id = this.getAttribute("customizationtarget");
          if (id)
            this._customizationTarget = document.getElementById(id);

          if (this._customizationTarget)
            this._customizationTarget.insertItem = this.insertItem.bind(this);
          else
            this._customizationTarget = this;

          return this._customizationTarget;
        ]]></getter>
      </property>

      <property name="toolbox" readonly="true">
        <getter><![CDATA[
          if (this._toolbox)
            return this._toolbox;

          let toolboxId = this.getAttribute("toolboxid");
          if (toolboxId) {
            let toolbox = document.getElementById(toolboxId);
            if (toolbox) {
              if (toolbox.externalToolbars.indexOf(this) == -1)
                toolbox.externalToolbars.push(this);

              this._toolbox = toolbox;
            }
          }

          if (!this._toolbox && this.parentNode &&
              this.parentNode.localName == "toolbox") {
            this._toolbox = this.parentNode;
          }

          return this._toolbox;
        ]]></getter>
      </property>

      <property name="currentSet">
        <getter><![CDATA[
          let currentWidgets = new Set();
          for (let node of this.customizationTarget.children) {
            let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
            if (realNode.getAttribute("skipintoolbarset") != "true") {
              currentWidgets.add(realNode.id);
            }
          }
          if (this.getAttribute("overflowing") == "true") {
            let overflowTarget = this.getAttribute("overflowtarget");
            let overflowList = this.ownerDocument.getElementById(overflowTarget);
            for (let node of overflowList.children) {
              let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
              if (realNode.getAttribute("skipintoolbarset") != "true") {
                currentWidgets.add(realNode.id);
              }
            }
          }
          let orderedPlacements = CustomizableUI.getWidgetIdsInArea(this.id);
          return orderedPlacements.filter((x) => currentWidgets.has(x)).join(',');
        ]]></getter>
        <setter><![CDATA[
          // Get list of new and old ids:
          let newVal = (val || '').split(',').filter(x => x);
          let oldIds = CustomizableUI.getWidgetIdsInArea(this.id);

          // Get a list of items only in the new list
          let newIds = newVal.filter(id => oldIds.indexOf(id) == -1);
          CustomizableUI.beginBatchUpdate();
          try {
            for (let newId of newIds) {
              oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
              let nextId = newId;
              let pos;
              do {
                // Get the next item
                nextId = newVal[newVal.indexOf(nextId) + 1];
                // Figure out where it is in the old list
                pos = oldIds.indexOf(nextId);
                // If it's not in the old list, repeat:
              } while (pos == -1 && nextId);
              if (pos == -1) {
                pos = null; // We didn't find anything, insert at the end
              }
              CustomizableUI.addWidgetToArea(newId, this.id, pos);
            }

            let currentIds = this.currentSet.split(',');
            let removedIds = currentIds.filter(id => newIds.indexOf(id) == -1 && newVal.indexOf(id) == -1);
            for (let removedId of removedIds) {
              CustomizableUI.removeWidgetFromArea(removedId);
            }
          } finally {
            CustomizableUI.endBatchUpdate();
          }
        ]]></setter>
      </property>


    </implementation>
  </binding>

  <binding id="toolbar-menubar-stub">
    <implementation>
      <property name="toolbox" readonly="true">
        <getter><![CDATA[
          if (this._toolbox)
            return this._toolbox;

          if (this.parentNode && this.parentNode.localName == "toolbox") {
            this._toolbox = this.parentNode;
          }

          return this._toolbox;
        ]]></getter>
      </property>
      <property name="currentSet" readonly="true">
        <getter><![CDATA[
          return this.getAttribute("defaultset");
        ]]></getter>
      </property>
      <method name="insertItem">
        <body><![CDATA[
          return null;
        ]]></body>
      </method>
    </implementation>
  </binding>

  <!-- The toolbar-menubar-autohide and toolbar-drag bindings are almost
       verbatim copies of their toolkit counterparts - they just inherit from
       the customizableui's toolbar binding instead of toolkit's. We're currently
       OK with the maintainance burden of having two copies of a binding, since
       the long term goal is to move the customization framework into toolkit. -->

  <binding id="toolbar-menubar-autohide"
           extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
    <implementation>
      <constructor>
        this._setInactive();
      </constructor>
      <destructor>
        this._setActive();
      </destructor>

      <field name="_inactiveTimeout">null</field>

      <field name="_contextMenuListener"><![CDATA[({
        toolbar: this,
        contextMenu: null,

        get active () {
          return !!this.contextMenu;
        },

        init: function (event) {
          let node = event.target;
          while (node != this.toolbar) {
            if (node.localName == "menupopup")
              return;
            node = node.parentNode;
          }

          let contextMenuId = this.toolbar.getAttribute("context");
          if (!contextMenuId)
            return;

          this.contextMenu = document.getElementById(contextMenuId);
          if (!this.contextMenu)
            return;

          this.contextMenu.addEventListener("popupshown", this, false);
          this.contextMenu.addEventListener("popuphiding", this, false);
          this.toolbar.addEventListener("mousemove", this, false);
        },
        handleEvent: function (event) {
          switch (event.type) {
            case "popupshown":
              this.toolbar.removeEventListener("mousemove", this, false);
              break;
            case "popuphiding":
            case "mousemove":
              this.toolbar._setInactiveAsync();
              this.toolbar.removeEventListener("mousemove", this, false);
              this.contextMenu.removeEventListener("popuphiding", this, false);
              this.contextMenu.removeEventListener("popupshown", this, false);
              this.contextMenu = null;
              break;
          }
        }
      })]]></field>

      <method name="_setInactive">
        <body><![CDATA[
          this.setAttribute("inactive", "true");
        ]]></body>
      </method>

      <method name="_setInactiveAsync">
        <body><![CDATA[
          this._inactiveTimeout = setTimeout(function (self) {
            if (self.getAttribute("autohide") == "true") {
              self._inactiveTimeout = null;
              self._setInactive();
            }
          }, 0, this);
        ]]></body>
      </method>

      <method name="_setActive">
        <body><![CDATA[
          if (this._inactiveTimeout) {
            clearTimeout(this._inactiveTimeout);
            this._inactiveTimeout = null;
          }
          this.removeAttribute("inactive");
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="DOMMenuBarActive"     action="this._setActive();"/>
      <handler event="popupshowing"         action="this._setActive();"/>
      <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
      <handler event="DOMMenuBarInactive"><![CDATA[
        if (!this._contextMenuListener.active)
          this._setInactiveAsync();
      ]]></handler>
    </handlers>
  </binding>

  <binding id="toolbar-drag"
           extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
    <implementation>
      <field name="_dragBindingAlive">true</field>
      <constructor><![CDATA[
        if (!this._draggableStarted) {
          this._draggableStarted = true;
          try {
            let tmp = {};
            Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
            let draggableThis = new tmp.WindowDraggingElement(this);
            draggableThis.mouseDownCheck = function(e) {
              return this._dragBindingAlive;
            };
          } catch (e) {}
        }
      ]]></constructor>
    </implementation>
  </binding>


<!-- This is a peculiar binding. It is here to deal with overlayed/inserted add-on content,
      and immediately direct such content elsewhere. -->
  <binding id="addonbar-delegating">
    <implementation>
      <constructor><![CDATA[
          // Reading these immediately so nobody messes with them anymore:
          this._delegatingToolbar = this.getAttribute("toolbar-delegate");
          this._wasCollapsed = this.getAttribute("collapsed") == "true";
          // Leaving those in here to unbreak some code:
          if (document.readyState == "complete") {
            this._init();
          } else {
            // Need to wait until XUL overlays are loaded. See bug 554279.
            let self = this;
            document.addEventListener("readystatechange", function onReadyStateChange() {
              if (document.readyState != "complete")
                return;
              document.removeEventListener("readystatechange", onReadyStateChange, false);
              self._init();
            }, false);
          }
      ]]></constructor>

      <method name="_init">
        <body><![CDATA[
          // Searching for the toolbox palette in the toolbar binding because
          // toolbars are constructed first.
          let toolbox = this.toolbox;
          if (toolbox && !toolbox.palette) {
            for (let node of toolbox.children) {
              if (node.localName == "toolbarpalette") {
                // Hold on to the palette but remove it from the document.
                toolbox.palette = node;
                toolbox.removeChild(node);
              }
            }
          }

          // pass the current set of children for comparison with placements:
          let children = [];
          for (let node of this.childNodes) {
            if (node.getAttribute("skipintoolbarset") != "true" && node.id) {
              // Force everything to be removable so that buildArea can chuck stuff
              // out if the user has customized things / we've been here before:
              if (!this._whiteListed.has(node.id)) {
                node.setAttribute("removable", "true");
              }
              children.push(node);
            }
          }
          CustomizableUI.registerToolbarNode(this, children);
          let existingMigratedItems = (this.getAttribute("migratedset") || "").split(',');
          for (let migratedItem of existingMigratedItems.filter((x) => !!x)) {
            this._currentSetMigrated.add(migratedItem);
          }
          this.evictNodes();
          // We can't easily use |this| or strong bindings for the observer fn here
          // because that creates leaky circular references when the node goes away,
          // and XBL destructors are unreliable.
          let mutationObserver = new MutationObserver(function(mutations) {
            if (!mutations.length) {
              return;
            }
            let toolbar = mutations[0].target;
            // Can't use our own attribute because we might not have one if we're set to
            // collapsed
            let areCustomizing = toolbar.ownerDocument.documentElement.getAttribute("customizing");
            if (!toolbar._isModifying && !areCustomizing) {
              toolbar.evictNodes();
            }
          });
          mutationObserver.observe(this, {childList: true});
        ]]></body>
      </method>
      <method name="evictNodes">
        <body><![CDATA[
          this._isModifying = true;
          let i = this.childNodes.length;
          while (i--) {
            let node = this.childNodes[i];
            if (this.childNodes[i].id) {
              this.evictNode(this.childNodes[i]);
            } else {
              node.remove();
            }
          }
          this._isModifying = false;
          this._updateMigratedSet();
        ]]></body>
      </method>
      <method name="evictNode">
        <parameter name="aNode"/>
        <body>
        <![CDATA[
          if (this._whiteListed.has(aNode.id) || CustomizableUI.isSpecialWidget(aNode.id)) {
            return;
          }
          const kItemMaxWidth = 100;
          let oldParent = aNode.parentNode;
          aNode.setAttribute("removable", "true");
          this._currentSetMigrated.add(aNode.id);

          let movedOut = false;
          if (!this._wasCollapsed) {
            try {
              let nodeWidth = aNode.getBoundingClientRect().width;
              if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) {
                throw new Error(aNode.id + " is too big (" + nodeWidth +
                                "px wide), moving to the palette");
              }
              CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar);
              movedOut = true;
            } catch (ex) {
              // This will throw if the node is too big, or can't be moved there for
              // some reason. Report this:
              Cu.reportError(ex);
            }
          }

          /* We won't have moved the widget if either the add-on bar was collapsed,
           * or if it was too wide to be inserted into the navbar. */
          if (!movedOut) {
            try {
              CustomizableUI.removeWidgetFromArea(aNode.id);
            } catch (ex) {
              Cu.reportError(ex);
              aNode.remove();
            }
          }

          // Surprise: addWidgetToArea(palette) will get you nothing if the palette
          // is not constructed yet. Fix:
          if (aNode.parentNode == oldParent) {
            let palette = this.toolbox.palette;
            if (palette && oldParent != palette) {
              palette.appendChild(aNode);
            }
          }
        ]]></body>
      </method>
      <method name="insertItem">
        <parameter name="aId"/>
        <parameter name="aBeforeElt"/>
        <parameter name="aWrapper"/>
        <body><![CDATA[
          if (aWrapper) {
            Cu.reportError("Can't insert " + aId + ": using insertItem " +
                           "no longer supports wrapper elements.");
            return null;
          }

          let widget = CustomizableUI.getWidget(aId);
          widget = widget && widget.forWindow(window);
          let node = widget && widget.node;
          if (!node) {
            return null;
          }

          this._isModifying = true;
          // Temporarily add it here so it can have a width, then ditch it:
          this.appendChild(node);
          this.evictNode(node);
          this._isModifying = false;
          this._updateMigratedSet();
          // We will now have moved stuff around; kick off some events
          // so add-ons know we've just moved their stuff:
          // XXXgijs: only in this window. It's hard to know for sure what's the right
          // thing to do here - typically insertItem is used on each window, so
          // this seems to make the most sense, even if some of the effects of
          // evictNode might affect multiple windows.
          CustomizableUI.dispatchToolboxEvent("customizationchange", {}, window);
          CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
          return node;
        ]]></body>
      </method>
      <method name="getMigratedItems">
        <body><![CDATA[
          return [... this._currentSetMigrated];
        ]]></body>
      </method>
      <method name="_updateMigratedSet">
        <body><![CDATA[
          let newMigratedItems = this.getMigratedItems().join(',');
          if (this.getAttribute("migratedset") != newMigratedItems) {
            this.setAttribute("migratedset", newMigratedItems);
            this.ownerDocument.persist(this.id, "migratedset");
          }
        ]]></body>
      </method>
      <property name="customizationTarget" readonly="true">
        <getter><![CDATA[
          return this;
        ]]></getter>
      </property>
      <property name="currentSet">
        <getter><![CDATA[
          return Array.from(this.children, node => node.id).join(",");
        ]]></getter>
        <setter><![CDATA[
          let v = val.split(',');
          let newButtons = v.filter(x => x && (!this._whiteListed.has(x) &&
                                               !CustomizableUI.isSpecialWidget(x) &&
                                               !this._currentSetMigrated.has(x)));
          for (let newButton of newButtons) {
            this._currentSetMigrated.add(newButton);
            this.insertItem(newButton);
          }
          this._updateMigratedSet();
        ]]></setter>
      </property>
      <property name="toolbox" readonly="true">
        <getter><![CDATA[
          if (!this._toolbox && this.parentNode &&
              this.parentNode.localName == "toolbox") {
            this._toolbox = this.parentNode;
          }

          return this._toolbox;
        ]]></getter>
      </property>
      <field name="_whiteListed" readonly="true">new Set(["addonbar-closebutton", "status-bar"])</field>
      <field name="_isModifying">false</field>
      <field name="_currentSetMigrated">new Set()</field>
    </implementation>
  </binding>
</bindings>
