<?xml version="1.0"?>

<!-- ***** BEGIN LICENSE BLOCK *****
   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
   -
   - The contents of this file are subject to the Mozilla Public License Version
   - 1.1 (the "License"); you may not use this file except in compliance with
   - the License. You may obtain a copy of the License at
   - http://www.mozilla.org/MPL/
   -
   - Software distributed under the License is distributed on an "AS IS" basis,
   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
   - for the specific language governing rights and limitations under the
   - License.
   -
   - The Original Code is toolkit code.
   -
   - The Initial Developer of the Original Code is
   - Netscape Communications Corporation.
   - Portions created by the Initial Developer are Copyright (C) 2002
   - the Initial Developer. All Rights Reserved.
   -
   - Contributor(s):
   -   Joe Hewitt <hewitt@netscape.com> (original author)
   -   Simon Bünzli <zeniko@gmail.com>
   -   Alexander Surkov <surkov.alexander@gmail.com>
   -
   - Alternatively, the contents of this file may be used under the terms of
   - either the GNU General Public License Version 2 or later (the "GPL"), or
   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
   - in which case the provisions of the GPL or the LGPL are applicable instead
   - of those above. If you wish to allow use of your version of this file only
   - under the terms of either the GPL or the LGPL, and not to allow others to
   - use your version of this file under the terms of the MPL, indicate your
   - decision by deleting the provisions above and replace them with the notice
   - and other provisions required by the GPL or the LGPL. If you do not delete
   - the provisions above, a recipient may use your version of this file under
   - the terms of any one of the MPL, the GPL or the LGPL.
   -
   - ***** END LICENSE BLOCK ***** -->

<bindings id="listboxBindings"
          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">

  <!--
    Interface binding that is base for bindings of xul:listbox and
    xul:richlistbox elements. This binding assumes that successors bindings
    will implement the following properties and methods:

    /** Return the number of items */
    readonly itemCount

    /** Return index of given item
    * @param aItem - given item element */
    getIndexOfItem(aItem)

    /** Return item at given index
    * @param aIndex - index of item element */
    getItemAtIndex(aIndex)

    /** Return count of item elements */
    getRowCount()

    /** Return count of visible item elements */
    getNumberOfVisibleRows()

    /** Return index of first visible item element */
    getIndexOfFirstVisibleRow()

    /** Return true if item of given index is visible
     * @param aIndex - index of item element
     *
     * @note XXX: this method should be removed after bug 364612 is fixed
     */
    ensureIndexIsVisible(aIndex)

    /** Return true if item element is visible
     * @param aElement - given item element */
    ensureElementIsVisible(aElement)

    /** Scroll list control to make visible item of given index
     * @param aIndex - index of item element
     *
     * @note XXX: this method should be removed after bug 364612 is fixed
     */
    scrollToIndex(aIndex)

    /** Create item element and append it to the end of listbox
     * @param aLabel - label of new item element
     * @param aValue - value of new item element */
    appendItem(aLabel, aValue)

    /** Create item element and insert it to given position
     * @param aIndex - insertion position
     * @param aLabel - label of new item element
     * @param aValue - value of new item element */
    insertItemAt(aIndex, aLabel, aValue)

    /** Scroll up/down one page
     * @param aDirection - specifies scrolling direction, should be either -1 or 1
     * @return the number of elements the selection scrolled
     */
    scrollOnePage(aDirection)

    /** Fire "select" event */
    _fireOnSelect()
   -->
   <binding id="listbox-base"
            extends="chrome://global/content/bindings/general.xml#basecontrol">

    <implementation implements="nsIDOMXULMultiSelectControlElement, nsIAccessibleProvider">
      <field name="_lastKeyTime">0</field>
      <field name="_incrementalString">""</field>

    <!-- nsIAccessibleProvider -->
      <property name="accessibleType" readonly="true">
        <getter>
          return Components.interfaces.nsIAccessibleProvider.XULListbox;
        </getter>
      </property>

    <!-- nsIDOMXULSelectControlElement -->
      <property name="selectedItem"
                onset="this.selectItem(val);">
        <getter>
        <![CDATA[
          return this.selectedItems.length > 0 ? this.selectedItems[0] : null;
        ]]>
        </getter>
      </property>

      <property name="selectedIndex">
        <getter>
        <![CDATA[
          if (this.selectedItems.length > 0)
            return this.getIndexOfItem(this.selectedItems[0]);
          return -1;
        ]]>
        </getter>
        <setter>
        <![CDATA[
          if (val >= 0) {
            this.selectItem(this.getItemAtIndex(val));
          } else {
            this.clearSelection();
            this.currentItem = null;
          }
        ]]>
        </setter>
      </property>

      <property name="value">
        <getter>
        <![CDATA[
          if (this.selectedItems.length > 0)
            return this.selectedItem.value;
          return null;
        ]]>
        </getter>
        <setter>
        <![CDATA[
          var kids = this.getElementsByAttribute("value", val);
          if (kids && kids.item(0))
            this.selectItem(kids[0]);
          return val;
        ]]>
        </setter>
      </property>

      <method name="removeItemAt">
        <parameter name="index"/>
        <body>
        <![CDATA[
          var remove = this.getItemAtIndex(index);
          if (remove)
            this.removeChild(remove);
          return remove;
        ]]>
        </body>
      </method>

    <!-- nsIDOMXULMultiSelectControlElement -->
      <property name="selType"
                onget="return this.getAttribute('seltype');"
                onset="this.setAttribute('seltype', val); return val;"/>

      <property name="currentItem" onget="return this._currentItem;">
        <setter>
          if (this._currentItem == val)
            return val;

          if (this._currentItem)
            this._currentItem.current = false;
          this._currentItem = val;

          if (val)
            val.current = true;

          return val;
        </setter>
      </property>

      <property name="currentIndex">
        <getter>
          return this.currentItem ? this.getIndexOfItem(this.currentItem) : -1;
        </getter>
        <setter>
        <![CDATA[
          if (val >= 0)
            this.currentItem = this.getItemAtIndex(val);
          else
            this.currentItem = null;
        ]]>
        </setter>
      </property>

      <field name="selectedItems">[]</field>

      <method name="addItemToSelection">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (this.selType != "multiple" && this.selectedCount)
            return;

          if (aItem.selected)
            return;

          this.selectedItems.push(aItem);
          aItem.selected = true;

          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <method name="removeItemFromSelection">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (!aItem.selected)
            return;

          for (var i = 0; i < this.selectedItems.length; ++i) {
            if (this.selectedItems[i] == aItem) {
              this.selectedItems.splice(i, 1);
              aItem.selected = false;
              break;
            }
          }

          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <method name="toggleItemSelection">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (aItem.selected)
            this.removeItemFromSelection(aItem);
          else
            this.addItemToSelection(aItem);
        ]]>
        </body>
      </method>

      <method name="selectItem">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          if (!aItem)
            return;

          if (this.selectedItems.length == 1 && this.selectedItems[0] == aItem)
            return;

          this._selectionStart = null;

          var suppress = this._suppressOnSelect;
          this._suppressOnSelect = true;

          this.clearSelection();
          this.addItemToSelection(aItem);
          this.currentItem = aItem;

          this._suppressOnSelect = suppress;
          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <method name="selectItemRange">
        <parameter name="aStartItem"/>
        <parameter name="aEndItem"/>
        <body>
        <![CDATA[
          if (this.selType != "multiple")
            return;

          if (!aStartItem)
            aStartItem = this._selectionStart ?
              this._selectionStart : this.currentItem;

          if (!aStartItem)
            aStartItem = aEndItem;

          var suppressSelect = this._suppressOnSelect;
          this._suppressOnSelect = true;

          this._selectionStart = aStartItem;

          var currentItem;
          var startIndex = this.getIndexOfItem(aStartItem);
          var endIndex = this.getIndexOfItem(aEndItem);
          if (endIndex < startIndex) {
            currentItem = aEndItem;
            aEndItem = aStartItem;
            aStartItem = currentItem;
          } else {
            currentItem = aStartItem;
          }

          while (currentItem) {
            this.addItemToSelection(currentItem);
            if (currentItem == aEndItem) {
              currentItem = this.getNextItem(currentItem, 1);
              break;
            }
            currentItem = this.getNextItem(currentItem, 1);
          }

          // Clear around new selection
          // Don't use clearSelection() because it causes a lot of noise
          // with respect to selection removed notifications used by the
          // accessibility API support.
          var userSelecting = this._userSelecting;
          this._userSelecting = false; // that's US automatically unselecting
          for (; currentItem; currentItem = this.getNextItem(currentItem, 1))
            this.removeItemFromSelection(currentItem);

          for (currentItem = this.getItemAtIndex(0); currentItem != aStartItem;
               currentItem = this.getNextItem(currentItem, 1))
            this.removeItemFromSelection(currentItem);
          this._userSelecting = userSelecting;

          this._suppressOnSelect = suppressSelect;

          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <method name="selectAll">
        <body>
          this._selectionStart = null;

          var suppress = this._suppressOnSelect;
          this._suppressOnSelect = true;

          var item = this.getItemAtIndex(0);
          while (item) {
            this.addItemToSelection(item);
            item = this.getNextItem(item, 1);
          }

          this._suppressOnSelect = suppress;
          this._fireOnSelect();
        </body>
      </method>

      <method name="invertSelection">
        <body>
          this._selectionStart = null;

          var suppress = this._suppressOnSelect;
          this._suppressOnSelect = true;

          var item = this.getItemAtIndex(0);
          while (item) {
            if (item.selected)
              this.removeItemFromSelection(item);
            else
              this.addItemToSelection(item);
            item = this.getNextItem(item, 1);
          }

          this._suppressOnSelect = suppress;
          this._fireOnSelect();
        </body>
      </method>

      <method name="clearSelection">
        <body>
        <![CDATA[
          if (this.selectedItems) {
            for (var i = this.selectedItems.length - 1; i >= 0; --i)
              this.selectedItems[i].selected = false;

            this.selectedItems.length = 0;
          }

          this._selectionStart = null;
          this._fireOnSelect();
        ]]>
        </body>
      </method>

      <property name="selectedCount" readonly="true"
                onget="return this.selectedItems.length;"/>

      <method name="getSelectedItem">
        <parameter name="aIndex"/>
        <body>
        <![CDATA[
          return aIndex < this.selectedItems.length ?
            this.selectedItems[aIndex] : null;
        ]]>
        </body>
      </method>

    <!-- Other public members -->
      <property name="disableKeyNavigation"
                onget="return this.hasAttribute('disableKeyNavigation');">
        <setter>
          if (val)
            this.setAttribute("disableKeyNavigation", "true");
          else
            this.removeAttribute("disableKeyNavigation");
          return val;
        </setter>
      </property>

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

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

      <method name="timedSelect">
        <parameter name="aItem"/>
        <parameter name="aTimeout"/>
        <body>
        <![CDATA[
          var suppress = this._suppressOnSelect;
          if (aTimeout != -1)
            this._suppressOnSelect = true;

          this.selectItem(aItem);

          this._suppressOnSelect = suppress;

          if (aTimeout != -1) {
            if (this._selectTimeout)
              window.clearTimeout(this._selectTimeout);
            this._selectTimeout =
              window.setTimeout(this._selectTimeoutHandler, aTimeout, this);
          }
        ]]>
        </body>
      </method>

      <method name="moveByOffset">
        <parameter name="aOffset"/>
        <parameter name="aIsSelecting"/>
        <parameter name="aIsSelectingRange"/>
        <body>
        <![CDATA[
          if ((aIsSelectingRange || !aIsSelecting) &&
              this.selType != "multiple")
            return;

          var newIndex = this.currentIndex + aOffset;
          if (newIndex < 0)
            newIndex = 0;

          var numItems = this.getRowCount();
          if (newIndex > numItems - 1)
            newIndex = numItems - 1;

          var newItem = this.getItemAtIndex(newIndex);
          // make sure that the item is actually visible/selectable
          if (this._userSelecting && newItem && !this._canUserSelect(newItem))
            newItem =
              aOffset > 0 ? this.getNextItem(newItem, 1) || this.getPreviousItem(newItem, 1) :
                            this.getPreviousItem(newItem, 1) || this.getNextItem(newItem, 1);
          if (newItem) {
            this.ensureIndexIsVisible(this.getIndexOfItem(newItem));
            if (aIsSelectingRange)
              this.selectItemRange(null, newItem);
            else if (aIsSelecting)
              this.selectItem(newItem);

            this.currentItem = newItem;
          }
        ]]>
        </body>
      </method>

    <!-- Private -->
      <method name="getNextItem">
        <parameter name="aStartItem"/>
        <parameter name="aDelta"/>
        <body>
        <![CDATA[
          while (aStartItem) {
            aStartItem = aStartItem.nextSibling;
            if (aStartItem && aStartItem instanceof
                Components.interfaces.nsIDOMXULSelectControlItemElement &&
                (!this._userSelecting || this._canUserSelect(aStartItem))) {
              --aDelta;
              if (aDelta == 0)
                return aStartItem;
            }
          }
          return null;
        ]]></body>
      </method>

      <method name="getPreviousItem">
        <parameter name="aStartItem"/>
        <parameter name="aDelta"/>
        <body>
        <![CDATA[
          while (aStartItem) {
            aStartItem = aStartItem.previousSibling;
            if (aStartItem && aStartItem instanceof
                Components.interfaces.nsIDOMXULSelectControlItemElement &&
                (!this._userSelecting || this._canUserSelect(aStartItem))) {
              --aDelta;
              if (aDelta == 0)
                return aStartItem;
            }
          }
          return null;
        ]]>
        </body>
      </method>

      <method name="_moveByOffsetFromUserEvent">
        <parameter name="aOffset"/>
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          if (!aEvent.getPreventDefault()) {
            this._userSelecting = true;
            this._mayReverse = true;
            this.moveByOffset(aOffset, !aEvent.ctrlKey, aEvent.shiftKey);
            this._userSelecting = false;
            this._mayReverse = false;
            aEvent.preventDefault();
          }
        ]]>
        </body>
      </method>

      <method name="_canUserSelect">
        <parameter name="aItem"/>
        <body>
        <![CDATA[
          var style = document.defaultView.getComputedStyle(aItem, "");
          return style.display != "none" && style.visibility == "visible";
        ]]>
        </body>
      </method>

      <method name="_selectTimeoutHandler">
        <parameter name="aMe"/>
        <body>
          aMe._fireOnSelect();
          aMe._selectTimeout = null;
        </body>
      </method>

      <field name="_suppressOnSelect">false</field>
      <field name="_userSelecting">false</field>
      <field name="_mayReverse">false</field>
      <field name="_selectTimeout">null</field>
      <field name="_currentItem">null</field>
      <field name="_selectionStart">null</field>
    </implementation>

    <handlers>
      <handler event="keypress" keycode="VK_UP" modifiers="control shift any"
               action="this._moveByOffsetFromUserEvent(-1, event);"
               group="system"/>
      <handler event="keypress" keycode="VK_DOWN" modifiers="control shift any"
               action="this._moveByOffsetFromUserEvent(1, event);"
               group="system"/>
      <handler event="keypress" keycode="VK_HOME" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(-this.currentIndex, event);
          this._mayReverse = false;
        ]]>
      </handler>
      <handler event="keypress" keycode="VK_END" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(this.getRowCount() - this.currentIndex - 1, event);
          this._mayReverse = false;
        ]]>
      </handler>
      <handler event="keypress" keycode="VK_PAGE_UP" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(this.scrollOnePage(-1), event);
          this._mayReverse = false;
        ]]>
      </handler>
      <handler event="keypress" keycode="VK_PAGE_DOWN" modifiers="control shift any"
               group="system">
        <![CDATA[
          this._mayReverse = true;
          this._moveByOffsetFromUserEvent(this.scrollOnePage(1), event);
          this._mayReverse = false;
        ]]>
      </handler>
      <handler event="keypress" key=" " modifiers="control" phase="target">
      <![CDATA[
        if (this.currentItem && this.selType == "multiple")
          this.toggleItemSelection(this.currentItem);
      ]]>
      </handler>
      <handler event="focus">
      <![CDATA[
        if (this.getRowCount() > 0) {
          if (this.currentIndex == -1) {
            this.currentIndex = this.getIndexOfFirstVisibleRow();
          }
          else {
            this.currentItem._fireEvent("DOMMenuItemActive");
          }
        }
        this._lastKeyTime = 0;
      ]]>
      </handler>
      <handler event="keypress" phase="target">
      <![CDATA[
        if (this.disableKeyNavigation || !event.charCode ||
            event.altKey || event.ctrlKey || event.metaKey)
          return;

        if (event.timeStamp - this._lastKeyTime > 1000)
          this._incrementalString = "";

        var key = String.fromCharCode(event.charCode).toLowerCase();
        this._incrementalString += key;
        this._lastKeyTime = event.timeStamp;

        // If all letters in the incremental string are the same, just
        // try to match the first one
        var incrementalString = /^(.)\1+$/.test(this._incrementalString) ?
                                RegExp.$1 : this._incrementalString;
        var length = incrementalString.length;

        var rowCount = this.getRowCount();
        var l = this.selectedItems.length;
        var start = l > 0 ? this.getIndexOfItem(this.selectedItems[l - 1]) : -1;
        // start from the first element if none was selected or from the one
        // following the selected one if it's a new or a repeated-letter search
        if (start == -1 || length == 1)
          start++;

        for (var i = 0; i < rowCount; i++) {
          var k = (start + i) % rowCount;
          var listitem = this.getItemAtIndex(k);
          if (!this._canUserSelect(listitem))
            continue;
          // allow richlistitems to specify the string being searched for
          var searchText = "searchLabel" in listitem ? listitem.searchLabel :
                           listitem.getAttribute("label"); // (see also bug 250123)
          searchText = searchText.substring(0, length).toLowerCase();
          if (searchText == incrementalString) {
            this.ensureIndexIsVisible(k);
            this.timedSelect(listitem, this._selectDelay);
            break;
          }
        }
      ]]>
      </handler>
    </handlers>
  </binding>


  <!-- Binding for xul:listbox element.
  -->
  <binding id="listbox"
           extends="#listbox-base">

    <resources>
      <stylesheet src="chrome://global/skin/listbox.css"/>
    </resources>

    <content>
      <children includes="listcols">
        <xul:listcols>
          <xul:listcol flex="1"/>
        </xul:listcols>
      </children>
      <xul:listrows>
        <children includes="listhead"/>
        <xul:listboxbody xbl:inherits="rows,size,minheight">
          <children includes="listitem"/>
        </xul:listboxbody> 
      </xul:listrows>
    </content>

    <implementation>

      <!-- ///////////////// public listbox members ///////////////// -->

      <property name="listBoxObject"
                onget="return this.boxObject.QueryInterface(Components.interfaces.nsIListBoxObject);"
                readonly="true"/>

      <!-- ///////////////// private listbox members ///////////////// -->

      <method name="_fireOnSelect">
        <body>
        <![CDATA[
          if (!this._suppressOnSelect && !this.suppressOnSelect) {
            var event = document.createEvent("Events");
            event.initEvent("select", true, true);
            this.dispatchEvent(event);
          }
        ]]>
        </body>
      </method>

      <constructor>
      <![CDATA[
        var count = this.itemCount;
        for (var index = 0; index < count; index++) {
          var item = this.getItemAtIndex(index);
          if (item.getAttribute("selected") == "true")
            this.selectedItems.push(item);
        }
      ]]>
      </constructor>

      <!-- ///////////////// nsIDOMXULSelectControlElement ///////////////// -->

      <method name="appendItem">
        <parameter name="aLabel"/>
        <parameter name="aValue"/>
        <body>
          const XULNS =
            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

          var item = this.ownerDocument.createElementNS(XULNS, "listitem");
          item.setAttribute("label", aLabel);
          item.setAttribute("value", aValue);
          this.appendChild(item);
          return item;
        </body>
      </method>

      <method name="insertItemAt">
        <parameter name="aIndex"/>
        <parameter name="aLabel"/>
        <parameter name="aValue"/>
        <body>
          const XULNS =
            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

          var item = this.ownerDocument.createElementNS(XULNS, "listitem");
          item.setAttribute("label", aLabel);
          item.setAttribute("value", aValue);
          var before = this.getItemAtIndex(aIndex);
          if (before)
            this.insertBefore(item, before);
          else
            this.appendChild(item);
          return item;
        </body>
      </method>

      <property name="itemCount" readonly="true"
                onget="return this.listBoxObject.getRowCount()"/>

      <!-- ///////////////// nsIListBoxObject ///////////////// -->
      <method name="getIndexOfItem">
        <parameter name="item"/>
        <body>
          return this.listBoxObject.getIndexOfItem(item);
        </body>
      </method>
      <method name="getItemAtIndex">
        <parameter name="index"/>
        <body>
          return this.listBoxObject.getItemAtIndex(index);
        </body>
      </method>
      <method name="ensureIndexIsVisible">
        <parameter name="index"/>
        <body>
          return this.listBoxObject.ensureIndexIsVisible(index);
        </body>
      </method>
      <method name="ensureElementIsVisible">
        <parameter name="element"/>
        <body>
          return this.ensureIndexIsVisible(this.listBoxObject.getIndexOfItem(element));
        </body>
      </method>
      <method name="scrollToIndex">
        <parameter name="index"/>
        <body>
          return this.listBoxObject.scrollToIndex(index);
        </body>
      </method>
      <method name="getNumberOfVisibleRows">
        <body>
          return this.listBoxObject.getNumberOfVisibleRows();
        </body>
      </method>
      <method name="getIndexOfFirstVisibleRow">
        <body>
          return this.listBoxObject.getIndexOfFirstVisibleRow();
        </body>
      </method>
      <method name="getRowCount">
        <body>
          return this.listBoxObject.getRowCount();
        </body>
      </method>

      <method name="scrollOnePage">
        <parameter name="direction"/>  <!-- Must be -1 or 1 -->
        <body>      
          <![CDATA[
            var pageOffset = this.getNumberOfVisibleRows() * direction;
            // skip over invisible elements - the user won't care about them
            for (var i = 0; i != pageOffset; i += direction) {
              var item = this.getItemAtIndex(this.currentIndex + i);
              if (item && !this._canUserSelect(item))
                pageOffset += direction;
            }
            var newTop = this.getIndexOfFirstVisibleRow() + pageOffset;
            if (direction == 1) {
              var maxTop = this.getRowCount() - this.getNumberOfVisibleRows();
              for (i = this.getRowCount(); i >= 0 && i > maxTop; i--) {
                item = this.getItemAtIndex(i);
                if (item && !this._canUserSelect(item))
                  maxTop--;
              }
              if (newTop >= maxTop)
                newTop = maxTop;
            }
            if (newTop < 0)
              newTop = 0;
            this.scrollToIndex(newTop);
            return pageOffset;          
          ]]>
        </body>
      </method>
    </implementation>
    
    <handlers>
      <handler event="keypress" key=" " phase="target">
        <![CDATA[
          if (this.currentItem) {
            if (this.currentItem.getAttribute("type") != "checkbox")
              this.addItemToSelection(this.currentItem);
            else if (!this.currentItem.disabled) {
              this.currentItem.checked = !this.currentItem.checked;
              this.currentItem.doCommand();
            }
          }
        ]]>
      </handler>

      <handler event="MozSwipeGesture">
        <![CDATA[
          // Figure out which index to show
          let targetIndex = 0;

          // Only handle swipe gestures up and down
          switch (event.direction) {
            case event.DIRECTION_DOWN:
              targetIndex = this.itemCount - 1;
              // Fall through for actual action
            case event.DIRECTION_UP:
              this.ensureIndexIsVisible(targetIndex);
              break;
          }
        ]]>
      </handler>
    </handlers>    
  </binding>

  <binding id="listrows">

    <resources>
      <stylesheet src="chrome://global/skin/listbox.css"/>
    </resources>

    <handlers>
      <handler event="DOMMouseScroll" phase="capturing">
      <![CDATA[
        if (event.axis == event.HORIZONTAL_AXIS)
          return;

        var listBox = this.parentNode.listBoxObject;
        var rows = event.detail;
        if (rows == UIEvent.SCROLL_PAGE_UP)
          rows = -listBox.getNumberOfVisibleRows();
        else if (rows == UIEvent.SCROLL_PAGE_DOWN)
          rows = listBox.getNumberOfVisibleRows();

        listBox.scrollByLines(rows);
        event.preventDefault();
      ]]>
      </handler>

      <handler event="MozMousePixelScroll" phase="capturing">
      <![CDATA[
        if (event.axis == event.HORIZONTAL_AXIS)
          return;

        // shouldn't be scrolled by pixel scrolling events before a line/page
        // scrolling event.
        event.preventDefault();
      ]]>
      </handler>
    </handlers>
  </binding>
  
  <binding id="listitem"
           extends="chrome://global/content/bindings/general.xml#basetext">
    <resources>
      <stylesheet src="chrome://global/skin/listbox.css"/>
    </resources>

    <content>
      <children>
        <xul:listcell xbl:inherits="label,crop,disabled,flexlabel"/>
      </children>
    </content>

    <implementation implements="nsIDOMXULSelectControlItemElement, nsIAccessibleProvider">
      <property name="current" onget="return this.getAttribute('current') == 'true';">
        <setter><![CDATA[
          if (val)
            this.setAttribute("current", "true");
          else
            this.removeAttribute("current");

          this._fireEvent(val ? "DOMMenuItemActive" : "DOMMenuItemInactive");

          return val;
        ]]></setter>
      </property>

      <!-- ///////////////// nsIAccessibleProvider ///////////////// -->

      <property name="accessibleType" readonly="true">
        <getter>
          <![CDATA[
            return Components.interfaces.nsIAccessibleProvider.XULListitem;
          ]]>
        </getter>
      </property>

      <!-- ///////////////// nsIDOMXULSelectControlItemElement ///////////////// -->
                
      <property name="value" onget="return this.getAttribute('value');"
                             onset="this.setAttribute('value', val); return val;"/>
      <property name="label" onget="return this.getAttribute('label');"
                             onset="this.setAttribute('label', val); return val;"/>
      
      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
        <setter><![CDATA[
          if (val)
            this.setAttribute("selected", "true");
          else
            this.removeAttribute("selected");

          return val;
        ]]></setter>
      </property>

      <property name="control">
        <getter><![CDATA[
          var parent = this.parentNode;
          while (parent) {
            if (parent instanceof Components.interfaces.nsIDOMXULSelectControlElement)
              return parent;
            parent = parent.parentNode;
          }
          return null;
        ]]></getter>
      </property>

      <method name="_fireEvent">
        <parameter name="name"/>
        <body>
        <![CDATA[
          var event = document.createEvent("Events");
          event.initEvent(name, true, true);
          this.dispatchEvent(event);
        ]]>
        </body>
      </method>
    </implementation>
    <handlers>
      <!-- If there is no modifier key, we select on mousedown, not
           click, so that drags work correctly. -->
      <handler event="mousedown">
      <![CDATA[
        var control = this.control;
        if (!control || control.disabled)
          return;
        if ((!event.ctrlKey
            ) && !event.shiftKey && !event.metaKey) {
          if (!this.selected) {
            control.selectItem(this);
          }
          control.currentItem = this;
        }
      ]]>
      </handler>

      <!-- On a click (up+down on the same item), deselect everything
           except this item. -->
      <handler event="click" button="0">
      <![CDATA[
        var control = this.control;
        if (!control || control.disabled)
          return;
        control._userSelecting = true;
        if (control.selType != "multiple") {
          control.selectItem(this);
        }
        else if (event.ctrlKey || event.metaKey) {
          control.toggleItemSelection(this);
          control.currentItem = this;
        }
        else if (event.shiftKey) {
          control.selectItemRange(null, this);
          control.currentItem = this;
        }
        else {
          /* We want to deselect all the selected items except what was
            clicked, UNLESS it was a right-click.  We have to do this
            in click rather than mousedown so that you can drag a
            selected group of items */

          // use selectItemRange instead of selectItem, because this
          // doesn't de- and reselect this item if it is selected
          control.selectItemRange(this, this);
        }
        control._userSelecting = false;
      ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="listitem-iconic"
           extends="chrome://global/content/bindings/listbox.xml#listitem">
    <content>
      <children>
        <xul:listcell class="listcell-iconic" xbl:inherits="label,image,crop,disabled,flexlabel"/>
      </children>
    </content>
  </binding>
  
  <binding id="listitem-checkbox"
           extends="chrome://global/content/bindings/listbox.xml#listitem">
    <content>
      <children>
        <xul:listcell type="checkbox" xbl:inherits="label,crop,checked,disabled,flexlabel"/>
      </children>
    </content>

    <implementation>
      <property name="checked"
                onget="return this.getAttribute('checked') == 'true';">
        <setter><![CDATA[
          if (val)
            this.setAttribute('checked', 'true');
          else
            this.removeAttribute('checked');
          var event = document.createEvent('Events');
          event.initEvent('CheckboxStateChange', true, true);
          this.dispatchEvent(event);
          return val;
        ]]></setter>
      </property>
    </implementation>

    <handlers> 
      <handler event="mousedown" button="0">
      <![CDATA[
        if (!this.disabled && !this.control.disabled) {
          this.checked = !this.checked;
          this.doCommand();
        }
      ]]>
      </handler>
    </handlers>
  </binding>
  
  <binding id="listitem-checkbox-iconic"
           extends="chrome://global/content/bindings/listbox.xml#listitem-checkbox">
    <content>
      <children>
        <xul:listcell type="checkbox" class="listcell-iconic" xbl:inherits="label,image,crop,checked,disabled,flexlabel"/>
      </children>
    </content>
  </binding>
  
  <binding id="listcell"
           extends="chrome://global/content/bindings/general.xml#basecontrol">

    <resources>
      <stylesheet src="chrome://global/skin/listbox.css"/>
    </resources>

    <content>
      <children>
        <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
      </children>
    </content>

    <implementation implements="nsIAccessibleProvider">
      <property name="accessibleType" readonly="true">
        <getter>
        <![CDATA[
          // Don't expose xul:listcell as cell accessible until listbox is
          // multicolumn.
          const Ci = Components.interfaces;
          const kNoAccessible = Ci.nsIAccessibleProvider.NoAccessible;
          const kListCellAccessible = Ci.nsIAccessibleProvider.XULListCell;

          var listitem = this.parentNode;
          if (!(listitem instanceof Ci.nsIDOMXULSelectControlItemElement))
            return kNoAccessible;

          var list = listitem.control;
          if (!list)
            return kNoAccessible;

          var listcolsElm = list.getElementsByTagName("listcols")[0];
          if (!listcolsElm)
            return kNoAccessible;

          var listcolElms = listcolsElm.getElementsByTagName("listcol");
          if (listcolElms.length <= 1)
            return kNoAccessible;

          return kListCellAccessible;
        ]]>
        </getter>
      </property>
    </implementation>
  </binding>

  <binding id="listcell-iconic"
           extends="chrome://global/content/bindings/listbox.xml#listcell">
    <content>
      <children>
        <xul:image class="listcell-icon" xbl:inherits="src=image"/>
        <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
      </children>
    </content>
  </binding>

  <binding id="listcell-checkbox"
           extends="chrome://global/content/bindings/listbox.xml#listcell">
    <content>
      <children>
        <xul:image class="listcell-check" xbl:inherits="checked,disabled"/>
        <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
      </children>
    </content>
  </binding>

  <binding id="listcell-checkbox-iconic"
           extends="chrome://global/content/bindings/listbox.xml#listcell-checkbox">
    <content>
      <children>
        <xul:image class="listcell-check" xbl:inherits="checked,disabled"/>
        <xul:image class="listcell-icon" xbl:inherits="src=image"/>
        <xul:label class="listcell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled" flex="1" crop="right"/>
      </children>
    </content>
  </binding>

  <binding id="listhead">

    <resources>
      <stylesheet src="chrome://global/skin/listbox.css"/>
    </resources>

    <content>
      <xul:listheaditem>
        <children includes="listheader"/>
      </xul:listheaditem>
    </content>

    <implementation implements="nsIAccessibleProvider">
      <property name="accessibleType" readonly="true">
        <getter>
          return Components.interfaces.nsIAccessibleProvider.XULListHead;
        </getter>
      </property>
    </implementation>
  </binding>

  <binding id="listheader" display="xul:button">

    <resources>
      <stylesheet src="chrome://global/skin/listbox.css"/>
    </resources>

    <content>
      <xul:image class="listheader-icon"/>
      <xul:label class="listheader-label" xbl:inherits="value=label,crop" flex="1" crop="right"/>
      <xul:image class="listheader-sortdirection" xbl:inherits="sortDirection"/>
    </content>

    <implementation implements="nsIAccessibleProvider">
      <property name="accessibleType" readonly="true">
        <getter>
          return Components.interfaces.nsIAccessibleProvider.XULListHeader;
        </getter>
      </property>
    </implementation>
  </binding>

</bindings>

