<?xml version="1.0"?>

<!DOCTYPE bindings [
  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
  %globalDTD;
]>

<bindings id="arrowscrollboxBindings"
   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="scrollbox-base" extends="chrome://global/content/bindings/general.xml#basecontrol">
    <resources>
      <stylesheet src="chrome://global/skin/scrollbox.css"/>
    </resources>
  </binding>

  <binding id="scrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content>
      <xul:box class="box-inherit scrollbox-innerbox" xbl:inherits="orient,align,pack,dir" flex="1">
        <children/>
      </xul:box>
    </content>
  </binding>

  <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content>
      <xul:autorepeatbutton class="autorepeatbutton-up"
                            anonid="scrollbutton-up"
                            collapsed="true"
                            xbl:inherits="orient"
                            oncommand="_autorepeatbuttonScroll(event);"/>
      <xul:scrollbox xbl:inherits="orient,align,pack,dir" flex="1" anonid="scrollbox">
        <children/>
      </xul:scrollbox>
      <xul:autorepeatbutton class="autorepeatbutton-down"
                            anonid="scrollbutton-down"
                            collapsed="true"
                            xbl:inherits="orient"
                            oncommand="_autorepeatbuttonScroll(event);"/>
    </content>

    <implementation>
      <destructor><![CDATA[
        this._stopSmoothScroll();
      ]]></destructor>

      <field name="_scrollbox">
        document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
      </field>
      <field name="_scrollButtonUp">
        document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-up");
      </field>
      <field name="_scrollButtonDown">
        document.getAnonymousElementByAttribute(this, "anonid", "scrollbutton-down");
      </field>

      <field name="__prefBranch">null</field>
      <property name="_prefBranch" readonly="true">
        <getter><![CDATA[
          if (this.__prefBranch === null) {
            this.__prefBranch = Components.classes['@mozilla.org/preferences-service;1']
                                          .getService(Components.interfaces.nsIPrefBranch2);
          }
          return this.__prefBranch;
        ]]></getter>
      </property>

      <field name="_scrollIncrement">null</field>
      <property name="scrollIncrement" readonly="true">
        <getter><![CDATA[
          if (this._scrollIncrement === null) {
            try {
              this._scrollIncrement = this._prefBranch
                                          .getIntPref("toolkit.scrollbox.scrollIncrement");
            }
            catch (ex) {
              this._scrollIncrement = 20;
            }
          }
          return this._scrollIncrement;
        ]]></getter>
      </property>

      <field name="_smoothScroll">null</field>
      <property name="smoothScroll">
        <getter><![CDATA[
          if (this._smoothScroll === null) {
            if (this.hasAttribute("smoothscroll")) {
              this._smoothScroll = (this.getAttribute("smoothscroll") == "true");
            } else {
              try {
                this._smoothScroll = this._prefBranch
                                         .getBoolPref("toolkit.scrollbox.smoothScroll");
              }
              catch (ex) {
                this._smoothScroll = true;
              }
            }
          }
          return this._smoothScroll;
        ]]></getter>
        <setter><![CDATA[
          this._smoothScroll = val;
          return val;
        ]]></setter>
      </property>

      <field name="_scrollBoxObject">null</field>
      <property name="scrollBoxObject" readonly="true">
        <getter><![CDATA[
          if (!this._scrollBoxObject) {
            this._scrollBoxObject =
              this._scrollbox.boxObject
                             .QueryInterface(Components.interfaces.nsIScrollBoxObject);
          }
          return this._scrollBoxObject;
        ]]></getter>
      </property>

      <field name="_isLTRScrollbox">
        document.defaultView.getComputedStyle(this._scrollbox, "").direction == "ltr";
      </field>

      <method name="ensureElementIsVisible">
        <parameter name="element"/>
        <body><![CDATA[
          if (!this.smoothScroll || this.getAttribute("orient") == "vertical") {
            this.scrollBoxObject.ensureElementIsVisible(element);
            return;
          }

          var rect = this._scrollbox.getBoundingClientRect();
          var containerStart = rect.left;
          var containerEnd = rect.right;
          rect = element.getBoundingClientRect();
          var elementStart = rect.left;
          var elementEnd = rect.right;
          var amountToScroll;

          if (elementStart < containerStart) {
            amountToScroll = elementStart - containerStart;
          } else if (containerEnd < elementEnd) {
            amountToScroll = elementEnd - containerEnd;
          } else if (this._isScrolling) {
            // decelerate if a currently-visible element is selected during the scroll
            const STOP_DISTANCE = 15;
            if (this._isScrolling == -1 && elementStart - STOP_DISTANCE < containerStart)
              amountToScroll = elementStart - containerStart;
            else if (this._isScrolling == 1 && containerEnd - STOP_DISTANCE < elementEnd)
              amountToScroll = elementEnd - containerEnd;
            else
              amountToScroll = this._isScrolling * STOP_DISTANCE;
          } else {
            return;
          }

          this._smoothScrollByPixels(amountToScroll);
        ]]></body>
      </method>

      <method name="_smoothScrollByPixels">
        <parameter name="amountToScroll"/>
        <body><![CDATA[
          this._stopSmoothScroll();

          // Positive amountToScroll makes us scroll right (elements fly left), negative scrolls left.
          var round;
          if (amountToScroll < 0) {
            this._isScrolling = -1;
            round = Math.floor;
          } else {
            this._isScrolling = 1;
            round = Math.ceil;
          }

          const FRAME_LENGTH = 60;

          function processFrame(self, scrollAmounts, off) {
            var distance = scrollAmounts.shift();

            // Skip frames if we aren't getting the desired frame rate.
            if (off > 0) {
              for (var i = Math.round(off / FRAME_LENGTH); i > 0; i--)
                distance += scrollAmounts.shift() || 0;
            }

            self.scrollBoxObject.scrollBy(distance, 0);
            if (!scrollAmounts.length)
              self._stopSmoothScroll();
          }

          // amountToScroll: total distance to scroll
          // scrollAmount: distance to move during the particular effect frame (60ms)
          var scrollAmount, scrollAmounts = [];
          if (amountToScroll > 2 || amountToScroll < -2) {
            scrollAmount = round(amountToScroll * 0.2);
            scrollAmounts.push(scrollAmount, scrollAmount, scrollAmount);
            amountToScroll -= 3 * scrollAmount;
          }
          while (this._isScrolling < 0 && amountToScroll < 0 ||
                 this._isScrolling > 0 && amountToScroll > 0) {
            amountToScroll -= (scrollAmount = round(amountToScroll * 0.5));
            scrollAmounts.push(scrollAmount);
          }
          this._smoothScrollTimer = setInterval(processFrame, FRAME_LENGTH, this, scrollAmounts);
          processFrame(this, scrollAmounts, 0);
        ]]></body>
      </method>

      <method name="scrollByIndex">
        <parameter name="index"/>
        <body><![CDATA[
          if (index == 0)
            return;
          if (this.getAttribute("orient") == "vertical") {
            this.scrollBoxObject.scrollByIndex(index);
            return;
          }

          var x;
          if (index > 0)
            x = this._scrollbox.getBoundingClientRect().right + 1;
          else
            x = this._scrollbox.getBoundingClientRect().left - 1;
          var nextElement = this._elementFromPoint(x);
          if (!nextElement)
            return;

          var targetElement;
          if (!this._isLTRScrollbox)
            index *= -1;

          while (index < 0 && nextElement) {
            targetElement = nextElement;
            nextElement = nextElement.previousSibling;
            index++;
          }
          while (index > 0 && nextElement) {
            targetElement = nextElement;
            nextElement = nextElement.nextSibling;
            index--;
          }

          this.ensureElementIsVisible(targetElement);
        ]]></body>
      </method>

      <method name="_getScrollableElements">
        <body><![CDATA[
          return this.hasChildNodes() ?
                   this.childNodes : document.getBindingParent(this).childNodes;
        ]]></body>
      </method>

      <method name="_elementFromPoint">
        <parameter name="aX"/>
        <body><![CDATA[
          var elements = this._getScrollableElements();
          if (!this._isLTRScrollbox) {
            elements = Array.slice(elements);
            elements.reverse();
          }
          var low = 0;
          var high = elements.length - 1;

          while (low <= high) {
            var mid = Math.floor((low + high) / 2);
            var element = elements[mid];
            var rect = element.getBoundingClientRect();
            if (rect.left > aX)
              high = mid - 1; 
            else if (rect.right < aX)
              low = mid + 1;
            else
              return element;
          }

          return null;
        ]]></body>
      </method>

      <method name="_autorepeatbuttonScroll">
        <parameter name="event"/>
        <body><![CDATA[
          var dir = event.originalTarget == this._scrollButtonUp ? -1 : 1;
          if (this.getAttribute("orient") == "horizontal" && !this._isLTRScrollbox)
            dir *= -1;

          this.scrollByPixels(this.scrollIncrement * dir);

          event.stopPropagation();
        ]]></body>
      </method>

      <method name="scrollByPixels">
        <parameter name="px"/>
        <body><![CDATA[
          if (this.getAttribute("orient") == "horizontal")
            this.scrollBoxObject.scrollBy(px, 0);
          else
            this.scrollBoxObject.scrollBy(0, px);
        ]]></body>
      </method>

      <!-- 0: idle
           1: scrolling right
          -1: scrolling left -->
      <field name="_isScrolling">0</field>
      <field name="_smoothScrollTimer">0</field>

      <method name="_stopSmoothScroll">
        <body><![CDATA[
          if (this._isScrolling) {
            clearInterval(this._smoothScrollTimer);
            this._isScrolling = 0;
          }
        ]]></body>
      </method>

      <method name="_updateScrollButtonsDisabledState">
        <body><![CDATA[
          var disableUpButton = false;
          var disableDownButton = false;

          if (this.getAttribute("orient") == "horizontal") {
            var width = {};
            this.scrollBoxObject.getScrolledSize(width, {});
            var xPos = {};
            this.scrollBoxObject.getPosition(xPos, {});
            if (xPos.value == 0) {
              // In the RTL case, this means the _last_ element in the
              // scrollbox is visible
              if (this._isLTRScrollbox) 
                disableUpButton = true;
              else
                disableDownButton = true;
            }
            else if (this._scrollbox.boxObject.width + xPos.value == width.value) {
              // In the RTL case, this means the _first_ element in the
              // scrollbox is visible
              if (this._isLTRScrollbox)
                disableDownButton = true;
              else
                disableUpButton = true;
            }
          }
          else {  // vertical scrollbox
            var height = {};
            this.scrollBoxObject.getScrolledSize({}, height);
            var yPos = {};
            this.scrollBoxObject.getPosition({}, yPos);
            if (yPos.value == 0)
              disableUpButton = true;
            else if (this._scrollbox.boxObject.height + yPos.value == height.value)
              disableDownButton = true;
          }

          if (this._scrollButtonUp.disabled != disableUpButton ||
              this._scrollButtonDown.disabled != disableDownButton) {
            this._scrollButtonUp.disabled = disableUpButton;
            this._scrollButtonDown.disabled = disableDownButton;

            var event = document.createEvent("Events");
            event.initEvent("UpdatedScrollButtonsDisabledState", true, false);
            this.dispatchEvent(event);
          }
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="DOMMouseScroll"><![CDATA[
        // prevent horizontal scrolling from scrolling a vertical scrollbox
        if (event.axis == event.HORIZONTAL_AXIS &&
            this.getAttribute("orient") != "horizontal")
          return;
        // We allow vertical scrolling to scroll a horizontal scrollbox
        // because many users have a vertical scroll wheel but no
        // horizontal support.

        this.scrollByIndex(event.detail);
        event.stopPropagation();
      ]]></handler>

      <handler event="underflow" phase="capturing"><![CDATA[
        // filter underflow events which were dispatched on nested scrollboxes
        if (event.target != this)
          return;

        // Ignore events that doesn't match our orientation.
        // Scrollport event orientation:
        //   0: vertical
        //   1: horizontal
        //   2: both
        if (this.getAttribute("orient") == "horizontal") {
          if (event.detail == 0) {
            return;
          }
        }
        else {    // vertical scrollbox
          if (event.detail == 1) {
            return;
          }
        }

        this._scrollButtonUp.collapsed = true;
        this._scrollButtonDown.collapsed = true;
        try {
          // See bug 341047 and comments in overflow handler as to why 
          // try..catch is needed here
          var childNodes = document.getAnonymousNodes(this._scrollbox);
          if (childNodes && childNodes.length)
            this.scrollBoxObject.ensureElementIsVisible(childNodes[0]);
        }
        catch(e) {
          this._scrollButtonUp.collapsed = false;
          this._scrollButtonDown.collapsed = false;
        }
      ]]></handler>

      <handler event="overflow" phase="capturing"><![CDATA[
        // filter underflow events which were dispatched on nested scrollboxes
        if (event.target != this)
          return;

        // Ignore events that doesn't match our orientation.
        // Scrollport event orientation:
        //   0: vertical
        //   1: horizontal
        //   2: both
        if (this.getAttribute("orient") == "horizontal") {
          if (event.detail == 0) {
            return;
          }
        }
        else {    // vertical scrollbox
          if (event.detail == 1) {
            return;
          }
        }

        this._scrollButtonUp.collapsed = false;
        this._scrollButtonDown.collapsed = false;
        try {
          // See bug 341047, the overflow event is dispatched when the 
          // scrollbox already is mostly destroyed. This causes some code in
          // _updateScrollButtonsDisabledState() to throw an error. It also
          // means that the scrollbarbuttons were uncollapsed when that should
          // not be happening, because the whole overflow event should not be
          // happening in that case.
          this._updateScrollButtonsDisabledState();
        } 
        catch(e) {
          this._scrollButtonUp.collapsed = true;
          this._scrollButtonDown.collapsed = true;
        }
      ]]></handler>

      <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/>
    </handlers>
  </binding>

  <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
    <content repeat="hover" chromedir="&locale.dir;">
      <xul:image class="autorepeatbutton-icon"/>
    </content>
  </binding>

  <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
    <content>
      <xul:toolbarbutton class="scrollbutton-up" collapsed="true"
                         xbl:inherits="orient"
                         anonid="scrollbutton-up"
                         onclick="_distanceScroll(event);"
                         onmousedown="_startScroll(-1);"
                         onmouseover="_continueScroll(-1);"
                         onmouseup="_stopScroll();"
                         onmouseout="_pauseScroll();"
                         chromedir="&locale.dir;"/>
      <xul:scrollbox xbl:inherits="orient,align,pack,dir" flex="1" anonid="scrollbox">
        <children/>
      </xul:scrollbox>
      <xul:toolbarbutton class="scrollbutton-down" collapsed="true"
                         xbl:inherits="orient"
                         anonid="scrollbutton-down"
                         onclick="_distanceScroll(event);"
                         onmousedown="_startScroll(1);"
                         onmouseover="_continueScroll(1);"
                         onmouseup="_stopScroll();"
                         onmouseout="_pauseScroll();"
                         chromedir="&locale.dir;"/>
    </content>
    <implementation implements="nsITimerCallback, nsIDOMEventListener">
      <constructor><![CDATA[
        try {
          this._scrollDelay = this._prefBranch
                                  .getIntPref("toolkit.scrollbox.clickToScroll.scrollDelay");
        }
        catch (ex) {
        }
      ]]></constructor>

      <destructor><![CDATA[
        // Release timer to avoid reference cycles.
        if (this._scrollTimer) {
          this._scrollTimer.cancel();
          this._scrollTimer = null;
        }
      ]]></destructor>

      <field name="_scrollIndex">0</field>
      <field name="_scrollDelay">150</field>

      <method name="notify">
        <parameter name="aTimer"/>
        <body>
        <![CDATA[
          if (!document)
            aTimer.cancel();

          if (this.smoothScroll)
            this.scrollByPixels(25 * this._scrollIndex);
          else
            this.scrollBoxObject.scrollByIndex(this._scrollIndex);
        ]]>
        </body>
      </method>

      <method name="_startScroll">
        <parameter name="index"/>
        <body><![CDATA[
          if (this.getAttribute("orient") == "horizontal" && !this._isLTRScrollbox)
            index *= -1;
          this._scrollIndex = index;
          var scrollDelay = this.smoothScroll ? 60 : this._scrollDelay;
          if (!this._scrollTimer)
            this._scrollTimer =
              Components.classes["@mozilla.org/timer;1"]
                        .createInstance(Components.interfaces.nsITimer);
          else
            this._scrollTimer.cancel();

          this._scrollTimer.initWithCallback(this, scrollDelay,
                                             this._scrollTimer.TYPE_REPEATING_SLACK);
          this.notify(this._scrollTimer);
          this._mousedown = true;
        ]]>
        </body>
      </method>

      <method name="_stopScroll">
        <body><![CDATA[
          if (this._scrollTimer)
            this._scrollTimer.cancel();
          this._mousedown = false;
          if (!this._scrollIndex || !this.smoothScroll)
            return;

          this.scrollByIndex(this._scrollIndex);
          this._scrollIndex = 0;
        ]]></body>
      </method>

      <method name="_pauseScroll">
        <body><![CDATA[
          if (this._mousedown) {
            this._stopScroll();
            this._mousedown = true;
            document.addEventListener("mouseup", this, false);
            document.addEventListener("blur", this, true);
          }
        ]]></body>
      </method>

      <method name="_continueScroll">
        <parameter name="index"/>
        <body><![CDATA[
          if (this._mousedown)
            this._startScroll(index);
        ]]></body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (aEvent.type == "mouseup" ||
              aEvent.type == "blur" && aEvent.target == document) {
            this._mousedown = false;
            document.removeEventListener("mouseup", this, false);
            document.removeEventListener("blur", this, true);
          }
        ]]></body>
      </method>

      <method name="_distanceScroll">
        <parameter name="aEvent"/>
        <body><![CDATA[
          if (this.getAttribute("orient") == "vertical" ||
              aEvent.detail < 2 || aEvent.detail > 3)
            return;

          var scrollLeft = (aEvent.originalTarget == this._scrollButtonUp);
          if (!this._isLTRScrollbox)
            scrollLeft = !scrollLeft;
          var targetElement;

          if (aEvent.detail == 2) {
            // scroll by the width of the scrollbox; make sure that the next
            // partly-hidden element will become fully visible.
            var rect = this._scrollbox.getBoundingClientRect();
            var x;
            if (scrollLeft)
              x = rect.left - (rect.right - rect.left);
            else
              x = rect.right + (rect.right - rect.left);
            targetElement = this._elementFromPoint(x);

            if (targetElement)
              targetElement = scrollLeft ?
                              targetElement.nextSibling :
                              targetElement.previousSibling;
          }

          if (!targetElement) {
            // scroll to the first resp. last element
            var elements = this._getScrollableElements();
            targetElement = (this._isLTRScrollbox ? scrollLeft : !scrollLeft) ?
                            elements.item(0) :
                            elements.item(elements.length-1);
          }

          this.ensureElementIsVisible(targetElement);
        ]]></body>
      </method>

    </implementation>
  </binding>
</bindings>
