<?xml version="1.0"?>

<bindings id="autocompleteBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="autocomplete"
           extends="chrome://global/content/bindings/textbox.xml#textbox">
    <resources>
      <stylesheet src="chrome://global/content/autocomplete.css"/>
      <stylesheet src="chrome://global/skin/autocomplete.css"/>
    </resources>

    <content sizetopopup="pref">
      <children includes="menupopup"/>

      <xul:hbox class="autocomplete-textbox-container" flex="1" align="center">
        <children includes="image|deck|stack|box">
          <xul:image class="autocomplete-icon" allowevents="true"/>
        </children>

        <xul:hbox class="textbox-input-box" flex="1" xbl:inherits="context,tooltiptext=inputtooltiptext">
          <children/>
          <html:input anonid="input" class="autocomplete-textbox textbox-input"
                      allowevents="true"
                      xbl:inherits="tooltiptext=inputtooltiptext,onfocus,onblur,value,type,maxlength,disabled,size,readonly,placeholder,tabindex,accesskey,mozactionhint,userAction"/>
        </xul:hbox>
        <children includes="hbox"/>
      </xul:hbox>

      <xul:dropmarker class="autocomplete-history-dropmarker" allowevents="true"
                      xbl:inherits="open,enablehistory" anonid="historydropmarker"/>

      <xul:popupset>
        <xul:panel type="autocomplete" anonid="popup"
                   ignorekeys="true" noautofocus="true" level="top"
                   xbl:inherits="for=id,nomatch"/>
      </xul:popupset>
    </content>

    <implementation implements="nsIDOMXULMenuListElement, nsIAccessibleProvider">

      <constructor><![CDATA[
        // XXX bug 90337 band-aid until we figure out what's going on here
        if (this.value != this.mInputElt.value)
          this.mInputElt.value = this.value;
        delete this.value;

        // listen for menubar activation
        window.top.addEventListener("DOMMenuBarActive", this.mMenuBarListener, true);

        // set default property values
        this.ifSetAttribute("timeout", 50);
        this.ifSetAttribute("maxrows", 5);
        this.ifSetAttribute("showpopup", true);
        this.ifSetAttribute("disableKeyNavigation", true);
        
        // initialize the search sessions
        if (this.hasAttribute("autocompletesearch"))
          this.initAutoCompleteSearch();
        if (this.hasAttribute("searchSessions"))
          this.initSearchSessions();
        
        // hack to work around lack of bottom-up constructor calling
        if ("initialize" in this.resultsPopup)
          this.resultsPopup.initialize();
      ]]></constructor>

      <destructor><![CDATA[
        this.clearResults(false);
        window.top.removeEventListener("DOMMenuBarActive", this.mMenuBarListener, true);
      ]]></destructor>
      
      <!-- =================== PUBLIC PROPERTIES =================== -->

      <property name="value"
                onget="return this.mInputElt.value;">
        <setter><![CDATA[
          this.ignoreInputEvent = true;
          this.mInputElt.value = val;
          this.ignoreInputEvent = false;
          var event = document.createEvent('Events');
          event.initEvent('ValueChange', true, true);
          this.mInputElt.dispatchEvent(event);
          return val;
        ]]></setter>
      </property>
  
      <property name="focused"
                onget="return this.getAttribute('focused') == 'true';"/>

      <!-- space-delimited string of search session types to use -->
      <property name="searchSessions" readonly="true" onget="return this.getAttribute('searchSessions') || ''"/>

      <method name="initSearchSessions">
        <body><![CDATA[
          var list = this.searchSessions.split(" ");
          for (var i = 0; i < list.length; i++) {
            var name = list[i];
            var contractid = "@mozilla.org/autocompleteSession;1?type=" + name;
            if (contractid in Components.classes) {
              try {
                var session =
                  Components.classes[contractid].getService(Components.interfaces.nsIAutoCompleteSession);
                this.addSession(session, name);
              } catch (e) {
                dump("### ERROR - unable to create search session \"" + name + "\".\n");
              }
            } else {
              dump("search session \"" + name + "\" not found - skipping.\n");
            }
          }
        ]]></body>
      </method>

      <method name="initAutoCompleteSearch">
        <body><![CDATA[
          var list = this.getAttribute("autocompletesearch").split(" ");
          for (var i = 0; i < list.length; i++) {
            var name = list[i];
            var contractid = "@mozilla.org/autocomplete/search;1?name=" + name;
            if (contractid in Components.classes) {
              try {
                var search =
                  Components.classes[contractid].getService(Components.interfaces.nsIAutoCompleteSearch);
                var session = new (this.mAutoCompleteSession)(search);
                this.addSession(session, name);
              } catch (e) {
                dump("### ERROR - unable to create search \"" + name + "\".\n");
              }
            } else {
              dump("search \"" + name + "\" not found - skipping.\n");
            }
          }
        ]]></body>
      </method>

      <!-- the number of sessions currently in use -->
      <field name="sessionCount">0</field>

      <!-- number of milliseconds after a keystroke before a search begins -->
      <property name="timeout"
                onset="this.setAttribute('timeout', val); return val;"
                onget="var t = parseInt(this.getAttribute('timeout')); return t ? t : 0;"/>

      <!-- maximum number of rows to display -->
      <property name="maxRows"
                onset="this.setAttribute('maxrows', val); return val;"
                onget="var t = parseInt(this.getAttribute('maxrows')); return t ? t : 0;"/>

      <!-- option for filling the textbox with the best match while typing 
           and selecting the difference -->
      <property name="autoFill"
                onset="this.setAttribute('autofill', val); return val;"
                onget="return this.getAttribute('autofill') == 'true';"/>

      <!-- if the resulting match string is not at the beginning of the typed string,
           this will optionally autofill like this "bar |>> foobar|" -->
      <property name="completeDefaultIndex"
                onset="this.setAttribute('completedefaultindex', val); return val;"
                onget="return this.getAttribute('completedefaultindex') == 'true';"/>

      <!--  if this attribute is set, allow different style for 
            non auto-completed lines -->
      <property name="highlightNonMatches"
                onset="this.setAttribute('highlightnonmatches', val); return val;"
                onget="return this.getAttribute('highlightnonmatches') == 'true';"/>

      <!-- toggles a second column in the results list which contains
           the string in the comment field of each autocomplete result -->
      <property name="showCommentColumn"
                onget="return this.getAttribute('showcommentcolumn') == 'true';">
        <setter><![CDATA[
          this.resultsPopup.showCommentColumn = val;
          this.setAttribute('showcommentcolumn', val); return val;
        ]]></setter>
      </property>

                
      <!-- option for completing to the default result whenever the user hits
           enter or the textbox loses focus -->
      <property name="forceComplete"
                onset="this.setAttribute('forcecomplete', val); return val;"
                onget="return this.getAttribute('forcecomplete') == 'true';"/>

      <!-- option to show the popup containing the results -->
      <property name="showPopup"
                onset="this.setAttribute('showpopup', val); return val;"
                onget="return this.getAttribute('showpopup') == 'true';"/>

      <!-- option to allow scrolling through the list via the tab key, rather than
           tab moving focus out of the textbox -->
      <property name="tabScrolling"
                onset="return this.setAttribute('tabscrolling', val); return val;"
                onget="return this.getAttribute('tabscrolling') == 'true';"/>

      <!-- option to turn off autocomplete -->
      <property name="disableAutoComplete"
                onset="this.setAttribute('disableautocomplete', val); return val;"
                onget="return this.getAttribute('disableautocomplete') == 'true';"/>

      <!-- option to completely ignore any blur events while  
           searches are still going on.  This is useful so that nothing
           gets autopicked if the window is required to lose focus for
           some reason (eg in LDAP autocomplete, another window may be
           brought up so that the user can enter a password to authenticate
           to an LDAP server).  -->
      <property name="ignoreBlurWhileSearching"
                onset="this.setAttribute('ignoreblurwhilesearching', val); return val;"
                onget="return this.getAttribute('ignoreblurwhilesearching') == 'true';"/>

      <property name="minResultsForPopup"
                onset="this.setAttribute('minresultsforpopup', val); return val;"
                onget="var t = this.getAttribute('minresultsforpopup'); return t ? parseInt(t) : 1;"/>
      
      <!-- state which indicates the current action being performed by the user.
           Possible values are : none, typing, scrolling -->
      <property name="userAction"
                onset="this.setAttribute('userAction', val); return val;"
                onget="return this.getAttribute('userAction');"/>
      
      <!-- state which indicates if the last search had no matches -->
      <field name="noMatch">true</field>

      <!-- state which indicates a search is currently happening -->
      <field name="isSearching">false</field>

      <!-- state which indicates a search timeout is current waiting -->
      <property name="isWaiting" 
                onget="return this.mAutoCompleteTimer != 0;"/>

      <!-- reference to the results popup element -->
      <field name="resultsPopup"><![CDATA[
        document.getAnonymousElementByAttribute(this, "anonid", "popup");
      ]]></field>

      <!-- nsIAccessibleProvider --> 
      <property name="accessibleType" readonly="true">
        <getter>
          <![CDATA[
            // Will be exposed as nsIAccessible::ROLE_AUTOCOMPLETE
            return Components.interfaces.nsIAccessibleProvider.XULCombobox;
          ]]>
        </getter>
      </property>

      <!-- nsIDOMXULMenuListElement properties -->

      <property name="editable" readonly="true" onget="return true;" />  

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

      <property name="label" readonly="true" onget="return this.mInputElt.value;"/>

      <property name="open">
        <getter>
          <![CDATA[
            return this.getAttribute('open') == 'true';
          ]]>
        </getter>
        <setter>
          <![CDATA[
            var historyPopup = document.getAnonymousElementByAttribute(this, "anonid", "historydropmarker");
            if (val) {
              this.setAttribute('open',true); 
              historyPopup.showPopup();
            }
            else {
              this.removeAttribute('open');
              historyPopup.hidePopup();
            }
          ]]>
        </setter>
      </property>

      <!-- =================== PRIVATE PROPERTIES =================== -->

      <field name="mSessions">({})</field>
      <field name="mListeners">({})</field>
      <field name="mLastResults">({})</field>
      <field name="mLastStatus">({})</field>
      <field name="mLastKeyCode">null</field>
      <field name="mAutoCompleteTimer">0</field>
      <field name="mMenuOpen">false</field>
      <field name="mFireAfterSearch">false</field>
      <field name="mFinishAfterSearch">false</field>
      <field name="mNeedToFinish">false</field>
      <field name="mNeedToComplete">false</field>
      <field name="mTransientValue">false</field>
      <field name="mView">null</field>
      <field name="currentSearchString">""</field>
      <field name="ignoreInputEvent">false</field>
      <field name="oninit">null</field>
      <field name="mDefaultMatchFilled">false</field>
      <field name="mFirstReturn">true</field>

      <field name="mMenuBarListener"><![CDATA[
        ({
          self: this,
          handleEvent: function(aEvent) {
            try {
              this.self.finishAutoComplete(false, false, aEvent);
              this.self.clearTimer();
              this.self.closeResultPopup();
            } catch (e) {
              window.top.removeEventListener("DOMMenuBarActive", this, true);
            }
          }
        })
      ]]></field>

      <field name="mAutoCompleteSession"><![CDATA[
        var session = function(aSession) { this.session = aSession };
        session.prototype = {
          session: null,
          param: this,
          onStartLookup: function(aSearchString, aPreviousSearchResult, aListener) {
            this.session.startSearch(aSearchString,
                         this.param.getAttribute("autocompletesearchparam") || "",
                         aPreviousSearchResult && aPreviousSearchResult.lastResult,
                         aListener);
          },
          onStopLookup: function() {
            this.session.stopSearch();
          },
          onAutoComplete: function() {
          }
        };
        session;
      ]]></field>

      <field name="mAutoCompleteListener"><![CDATA[
        var listener = function(aName) { this.sessionName = aName };
        listener.prototype = {
          param: this,
          sessionName: null,
          lastResult: null,
          lastItems: [],
          onAutoComplete: function(aResults, aStatus) 
          {
            this.param.processResults(this.sessionName, aResults, aStatus);
          },
          onSearchResult: function(aSearch, aResult)
          {
            this.lastResult = aResult;
            this.lastItems = new Array(aResult.matchCount);
            const nsIAutoCompleteStatus = Components.interfaces.nsIAutoCompleteStatus;
            var status = nsIAutoCompleteStatus.failed;
            if (aResult.errorDescription) {
              status = nsIAutoCompleteStatus.failureItems;
              this.lastItems = [{
                value: aResult.errorDescription,
                comment: null,
                className: null,
                param: null
              }];
            }
            else if (aResult.searchResult == aResult.RESULT_IGNORED)
              status = nsIAutoCompleteStatus.ignored;
            else if (aResult.searchResult == aResult.RESULT_NOMATCH)
              status = nsIAutoCompleteStatus.noMatch;
            else if (aResult.searchResult == aResult.RESULT_SUCCESS)
              status = nsIAutoCompleteStatus.matchFound;
            else if (aResult.searchResult == aResult.RESULT_NOMATCH_ONGOING)
              status = nsIAutoCompleteStatus.noMatchYet;
            else if (aResult.searchResult == aResult.RESULT_SUCCESS_ONGOING)
              status = nsIAutoCompleteStatus.matchSoFar;
            this.param.processResults(this.sessionName, {
              searchString: aResult.searchString,
              items: this,
              defaultItemIndex: aResult.defaultIndex,
              param: null,
              lastResult: aResult
            }, status);
          },
          Count: function() {
            return this.lastItems.length;
          },
          QueryElementAt: function(aIndex, aIID) {
            if (aIndex < 0 || aIndex >= this.lastItems.length)
              return null;
            if (!this.lastItems[aIndex]) {
              this.lastItems[aIndex] = {
                value: this.lastResult.getValueAt(aIndex),
                comment: this.lastResult.getCommentAt(aIndex),
                className: this.lastResult.getStyleAt(aIndex),
                param: null
              }
            }
            return this.lastItems[aIndex];
          }
        };
        listener;
      ]]></field>

      <field name="mInputElt"><![CDATA[
        document.getAnonymousElementByAttribute(this, "anonid", "input");
      ]]></field>

      <field name="mMenuAccessKey"><![CDATA[
        Components.classes["@mozilla.org/preferences-service;1"]
                 .getService(Components.interfaces.nsIPrefBranch)
                 .getIntPref("ui.key.menuAccessKey");
      ]]></field>

      <!-- =================== PUBLIC METHODS =================== -->

      <!-- get the result object from the autocomplete results from a specific session -->
      <method name="getResultAt">
        <parameter name="aIndex"/>
        <body><![CDATA[
          var obj = this.convertIndexToSession(aIndex);
          if (obj && this.mLastResults[obj.session]) {
            const nsIAutoCompleteItem = Components.interfaces.nsIAutoCompleteItem;
            if (obj.index >= 0) {
              var item = this.mLastResults[obj.session].items.QueryElementAt(obj.index, nsIAutoCompleteItem);
              return item;
            }
          }
          return null;
        ]]></body>
      </method>

      <!-- get the autocomplete session status returned by the session
           that a given item came from -->
      <method name="getSessionStatusAt">
        <parameter name="aIndex"/>
        <body><![CDATA[
          var obj = this.convertIndexToSession(aIndex);
          return obj ? this.mLastStatus[obj.session] : null;
        ]]></body>
      </method>


      <!-- get a value from the autocomplete results as a string via an absolute index-->
      <method name="getResultValueAt">
        <parameter name="aIndex"/>
        <body><![CDATA[
          var obj = this.convertIndexToSession(aIndex);
          return obj ? this.getSessionValueAt(obj.session, obj.index) : null;
        ]]></body>
      </method>

      <!-- get the result object from the autocomplete results from a specific session -->
      <method name="getSessionResultAt">
        <parameter name="aSession"/>
        <parameter name="aIndex"/>
        <body><![CDATA[
          var session = this.mLastResults[aSession];
          if (session) {
            var item = session.items.QueryElementAt(aIndex, Components.interfaces.nsIAutoCompleteItem);
            return item;
          }
          return null;
        ]]></body>
      </method>

      <!-- get a value from the autocomplete results as a string from a specific session -->
      <method name="getSessionValueAt">
        <parameter name="aSession"/>
        <parameter name="aIndex"/>
        <body><![CDATA[
          var result = this.getSessionResultAt(aSession, aIndex);
          if (result)
            return result.value;
          return null;
        ]]></body>
      </method>

      <!-- get the total number of results overall -->
      <method name="getResultCount">
        <body><![CDATA[
          return this.view.rowCount;
        ]]></body>
      </method>
      
      <!-- get a session object by index -->
      <method name="getSession">
        <parameter name="aIndex"/>
        <body><![CDATA[
          var idx = 0;
          for (var name in this.mSessions) {
            if (idx == aIndex)
              return this.mSessions[name];
            ++idx;
          }
          return null;
        ]]></body>
      </method>

      <!-- get a session object by name -->
      <method name="getSessionByName">
        <parameter name="aSessionName"/>
        <body><![CDATA[
          return this.mSessions[aSessionName];
        ]]></body>
      </method>

      <!-- add a session by reference -->
      <method name="addSession">
        <parameter name="aSession"/>
        <parameter name="aName"/>
        <body><![CDATA[
          ++this.sessionCount;
          var name = aName || "anon_"+this.sessionCount;
          this.mSessions[name] = aSession;
          this.mListeners[name] = new (this.mAutoCompleteListener)(name);
          this.mLastResults[name] = null;
          this.mLastStatus[name] = null;
          return this.mSessions[name];
        ]]></body>
      </method>

      <!-- remove a session by reference -->
      <method name="removeSession">
        <parameter name="aSession"/>
        <body><![CDATA[
          for (var name in this.mSessions) {
            if (this.mSessions[name] == aSession) {
              delete this.mSessions[name];
              delete this.mListeners[name];
              delete this.mLastResults[name];
              delete this.mLastStatus[name];
              --this.sessionCount;
              break;
            }
          }
        ]]></body>
      </method>

      <!-- make this widget listen to all of the same autocomplete sessions 
           from another autocomplete widget -->
      <method name="syncSessions">
        <parameter name="aCopyFrom"/>
        <body><![CDATA[
          this.sessionCount = aCopyFrom.sessionCount;
          this.mSessions = {};
          this.mListeners = {};
          this.mLastResults = {};
          this.mLastStatus = {};
          for (var name in aCopyFrom.mSessions) {
            this.mSessions[name] = aCopyFrom.mSessions[name];
            this.mListeners[name] = new (this.mAutoCompleteListener)(name);
            this.mLastResults[name] = null;
            this.mLastStatus[name] = null;
          }
        ]]></body>
      </method>

      <!-- get the first session that has results -->
      <method name="getDefaultSession">
        <body><![CDATA[
          for (var name in this.mLastResults) {
            var results = this.mLastResults[name];
            var status = this.mLastStatus[name];
            if (results && results.items.Count() > 0 && status !=
                Components.interfaces.nsIAutoCompleteStatus.failureItems)
              return name;
          }
          return null;
        ]]></body>
      </method>

      <!-- empty the cached result data and empty the results popup -->
      <method name="clearResults">
        <parameter name="aInvalidate"/>
        <body><![CDATA[
          this.clearResultData();
          this.clearResultElements(aInvalidate);
        ]]></body>
      </method>

      <!-- =================== PRIVATE METHODS =================== -->
  
      <!-- ::::::::::::: session searching ::::::::::::: -->

      <!--  -->
      <method name="callListener">
        <parameter name="me"/>
        <parameter name="aAction"/>
        <body><![CDATA[
          // bail if the binding was detached or the element removed from
          // document during the timeout
          if (!("startLookup" in me) || !me.ownerDocument || !me.parentNode)
            return;

          me.clearTimer();
            
          if (me.disableAutoComplete)
            return;

          switch (aAction) {
            case "startLookup":
              me.startLookup();
              break;

            case "stopLookup":
              me.stopLookup();
              break;

            case "autoComplete":
              me.autoComplete();
              break;
          }
        ]]></body>
      </method>

      <!--  -->
      <method name="startLookup">
        <body><![CDATA[
          var str = this.currentSearchString;
          if (!str) {
            this.clearResults(false);
            this.closeResultPopup();
            return;
          }

          this.isSearching = true;
          this.mFirstReturn = true;
          this.mSessionReturns = this.sessionCount;
          this.mFailureItems = 0;
          this.mDefaultMatchFilled = false; // clear out our prefill state.

          // tell each session to start searching...
          for (var name in this.mSessions)
            try {
              this.mSessions[name].onStartLookup(str, this.mLastResults[name], this.mListeners[name]);
            } catch (e) {
              --this.mSessionReturns;
              this.searchFailed();
            }
        ]]></body>
      </method>

      <!--  -->
      <method name="stopLookup">
        <body><![CDATA[
          for (var name in this.mSessions)
            this.mSessions[name].onStopLookup();
        ]]></body>
      </method>

      <!--  -->
      <method name="autoComplete">
        <body><![CDATA[
          for (var name in this.mSessions)
            this.mSessions[name].onAutoComplete(this.value, 
                                                this.mLastResults[name],
                                                this.mListeners[name]);
        ]]></body>
      </method>

      <!--  -->
      <method name="processResults">
        <parameter name="aSessionName"/>
        <parameter name="aResults"/>
        <parameter name="aStatus"/>
        <body><![CDATA[
          if (this.disableAutoComplete)
            return;

          if (aStatus != Components.interfaces.nsIAutoCompleteStatus.noMatchYet &&
              aStatus != Components.interfaces.nsIAutoCompleteStatus.matchSoFar)
            --this.mSessionReturns;

          this.mLastStatus[aSessionName] = aStatus;

          // check the many criteria for failure
          if (aStatus == Components.interfaces.nsIAutoCompleteStatus.failed ||
              aStatus == Components.interfaces.nsIAutoCompleteStatus.ignored || 
              aStatus == Components.interfaces.nsIAutoCompleteStatus.noMatch ||
              aStatus == Components.interfaces.nsIAutoCompleteStatus.noMatchYet ||
              aResults == null ||
              aResults.items.Count() == 0 ||
              aResults.searchString != this.currentSearchString)
          {
            this.mLastResults[aSessionName] = null;
            if (this.mFirstReturn)
              this.clearResultElements(false);
            this.mFirstReturn = false;
            this.searchFailed();
            return;
          }
          if (aStatus == Components.interfaces.nsIAutoCompleteStatus.failureItems)
            ++this.mFailureItems;
          
          if (this.mFirstReturn) {
            if (this.view.mTree)
              this.view.mTree.beginUpdateBatch();
            this.clearResultElements(false); // clear results, but don't repaint yet
          }

          // always call openResultPopup...we may not have opened it
          // if a previous search session didn't return enough search results.
          // it's smart and doesn't try to open itself multiple times...
          // be sure to add our result elements before calling openResultPopuup as we need
          // to know the total # of results found so far.
          this.addResultElements(aResults, this.mLastResults[aSessionName]);
          this.mLastResults[aSessionName] = aResults;

          this.autoFillInput(aSessionName, aResults, false);
          if (this.mFirstReturn && this.view.mTree)
            this.view.mTree.endUpdateBatch();
          this.openResultPopup();
          this.mFirstReturn = false;
         
          // if this is the last session to return... 
          if (this.mSessionReturns == 0) 
            this.postSearchCleanup();
               
          if (this.mFinishAfterSearch)
            this.finishAutoComplete(false, this.mFireAfterSearch, null);
        ]]></body>
      </method>

      <!-- called each time a search fails, except when failure items need
           to be displayed. If all searches have failed, clear the list
           and close the popup -->
      <method name="searchFailed">
        <body><![CDATA[
          // if all searches are done and they all failed...
          if (this.mSessionReturns == 0 && this.getResultCount() == 0) {
            if (this.minResultsForPopup == 0) {
              this.clearResults(true); // clear data and repaint empty
              this.openResultPopup();
            } else {
              this.closeResultPopup(); 
            }
          }

          // if it's the last session to return, time to clean up...
          if (this.mSessionReturns == 0)
            this.postSearchCleanup();
        ]]></body>
      </method>

      <!-- does some stuff after a search is done (success or failure) -->
      <method name="postSearchCleanup">
        <body><![CDATA[
          this.isSearching = false;

          // figure out if there are no matches in all search sessions
          var failed = true;
          for (var name in this.mSessions) {
            if (this.mLastResults[name])
              failed = this.mLastResults[name].items.Count() < 1;
            if (!failed)
              break;
          }
          this.noMatch = failed;
          if (this.noMatch)
            this.setAttribute("nomatch", true);
          else
            this.removeAttribute("nomatch");
          
          // if we have processed all of our searches, and none of them gave us a default index,
          // then we should try to auto fill the input field with the first match. 
          // note: autoFillInput is smart enough to kick out if we've already prefilled something...
          if (!this.noMatch) {
             var defaultSession = this.getDefaultSession();
             if (defaultSession)
                this.autoFillInput(defaultSession, this.mLastResults[defaultSession], true);         
          }

	  // Now we've finished, fire an event to let everyone know
	  this._fireEvent("searchcomplete");
        ]]></body>
      </method>

      <!-- when the focus exits the widget or user hits return, 
           determine what value to leave in the textbox -->
      <method name="finishAutoComplete">
        <parameter name="aForceComplete"/>
        <parameter name="aFireTextCommand"/>
        <parameter name="aTriggeringEvent"/>
        <body><![CDATA[
          this.mFinishAfterSearch = false;
          this.mFireAfterSearch = false;
          if (this.mNeedToFinish && !this.disableAutoComplete) {
            // set textbox value to either override value, or default search result 
            var val = this.resultsPopup.overrideValue;
            if (val) {
              this.setTextValue(val);
              this.mNeedToFinish = false;
            } else if (this.mTransientValue || 
                       !(this.forceComplete ||
                        (aForceComplete &&
                         this.mDefaultMatchFilled &&
                         this.mNeedToComplete))) {
              this.mNeedToFinish = false;
            } else if (this.isWaiting) {
              // if the user typed, the search results are out of date, so let
              // the search finish, and tell it to come back here when it's done
              this.mFinishAfterSearch = true;
              this.mFireAfterSearch = aFireTextCommand;
              return;
            } else {
              // we want to use the default item index for the first session which gave us a valid
              // default item index...
              for (var name in this.mLastResults) {
                var results = this.mLastResults[name];
                if (results && results.items.Count() > 0 && results.defaultItemIndex != -1)
                {
                  val = this.getSessionValueAt(name, results.defaultItemIndex);
                  this.setTextValue(val);
                  this.mDefaultMatchFilled = true;
                  this.mNeedToFinish = false;
                  break;
                }
              }

              if (this.mNeedToFinish) {
                // if a search is happening at this juncture, bail out of this function
                // and let the search finish, and tell it to come back here when it's done
                if (this.isSearching) {
                  this.mFinishAfterSearch = true;
                  this.mFireAfterSearch = aFireTextCommand;
                  return;
                }

                this.mNeedToFinish = false;
                var defaultSession = this.getDefaultSession();
                if (defaultSession)
                {
                    // preselect the first one
                    var first = this.getSessionValueAt(defaultSession, 0);
                    this.setTextValue(first);
                    this.mDefaultMatchFilled = true;
                }
              }
            }

            this.stopLookup();

            if (!this.noMatch)
              this.autoComplete();

            this.closeResultPopup();
          }
          
          this.mNeedToComplete = false;
          this.clearTimer();

          if (aFireTextCommand)
            this._fireEvent("textentered", this.userAction, aTriggeringEvent);
        ]]></body>
      </method>

      <!--  when the user clicks an entry in the autocomplete popup -->
      <method name="onResultClick">
        <body><![CDATA[
         // set textbox value to either override value, or the clicked result
          var errItem=null;
          var val = this.resultsPopup.overrideValue;
            if (val)
              this.setTextValue(val);
          else if (this.resultsPopup.selectedIndex != -1) {
            if (this.getSessionStatusAt(this.resultsPopup.selectedIndex) ==
                Components.interfaces.nsIAutoCompleteStatus.failureItems) {
              this.setTextValue(this.currentSearchString);
              this.mTransientValue = true;
              errItem = this.getResultAt(this.resultsPopup.selectedIndex);
            } else { 
              this.setTextValue(this.getResultValueAt(
                                             this.resultsPopup.selectedIndex));
            }
          }

          if (!this.noMatch)
            this.autoComplete();

          this.mNeedToFinish = false;
          this.mNeedToComplete = false;
          
          this.closeResultPopup();

          this.currentSearchString = "";

          if (errItem)
            this._fireEvent("errorcommand", errItem);
          this._fireEvent("textentered", "clicking");
        ]]></body>
      </method>

      <!-- when the user hits escape, revert the previously typed value in the textbox -->
      <method name="undoAutoComplete">
        <body><![CDATA[
          var val = this.currentSearchString;

          var ok = this._fireEvent("textreverted");
          if ((ok || ok == undefined) && val)
            this.setTextValue(val);

          this.userAction = "typing";

          this.currentSearchString = this.value;
          this.mNeedToComplete = false;
        ]]></body>
      </method>

      <!-- convert an absolute result index into a session name/index pair -->
      <method name="convertIndexToSession">
        <parameter name="aIndex"/>
        <body><![CDATA[
          var idx = 0;
          for (var name in this.mLastResults) {
            if (this.mLastResults[name]) {
              if ((idx+this.mLastResults[name].items.Count())-1 >= aIndex) {
                return {session: name, index: aIndex-idx};
              }
              idx += this.mLastResults[name].items.Count();
            }
          }
          return null;
        ]]></body>
      </method>

      <!-- ::::::::::::: user input handling ::::::::::::: -->

      <!--  -->
      <method name="processInput">
        <body><![CDATA[
          // stop current lookup in case it's async.
          this.stopLookup();
          // stop the queued up lookup on a timer
          this.clearTimer();

          if (this.disableAutoComplete)
            return;

          this.userAction = "typing";
          this.mFinishAfterSearch = false;
          this.mNeedToFinish = true;
          this.mTransientValue = false;
          this.mNeedToComplete = true;
          var str = this.value;
          this.currentSearchString = str;
          this.resultsPopup.clearSelection();
          
          this.mAutoCompleteTimer = setTimeout(this.callListener, this.timeout, this, "startLookup");
        ]]></body>
      </method>

      <!--  -->
      <method name="processKeyPress">
        <parameter name="aEvent"/>
        <body><![CDATA[
          this.mLastKeyCode = aEvent.keyCode;
          
          var killEvent = false;
          
          switch (aEvent.keyCode) {
            case KeyEvent.DOM_VK_TAB:
              if (this.tabScrolling) {
                // don't kill this event if alt-tab or ctrl-tab is hit
                if (!aEvent.altKey && !aEvent.ctrlKey) {
                  killEvent = this.mMenuOpen;
                  if (killEvent)
                    this.keyNavigation(aEvent);
                }
              } 
              break;              
              
            case KeyEvent.DOM_VK_RETURN:

              // if this is a failure item, save it for fireErrorCommand
              var errItem = null;
              if (this.resultsPopup.selectedIndex != -1 && 
                  this.getSessionStatusAt(this.resultsPopup.selectedIndex) ==
                  Components.interfaces.nsIAutoCompleteStatus.failureItems) {
                errItem = this.getResultAt(this.resultsPopup.selectedIndex);
              }

              killEvent = this.mMenuOpen;
              this.finishAutoComplete(true, true, aEvent);
              this.closeResultPopup();
              if (errItem) {
                  this._fireEvent("errorcommand", errItem);
              }
              break;

            case KeyEvent.DOM_VK_ESCAPE:
              this.clearTimer();
              killEvent = this.mMenuOpen;
              this.undoAutoComplete();
              this.closeResultPopup();
              break;
  
            case KeyEvent.DOM_VK_LEFT:
            case KeyEvent.DOM_VK_RIGHT:
            case KeyEvent.DOM_VK_HOME:
            case KeyEvent.DOM_VK_END:
              this.finishAutoComplete(true, false, aEvent);
              this.clearTimer();
              this.closeResultPopup();
              break;

            case KeyEvent.DOM_VK_DOWN:
              if (!aEvent.altKey) {
                this.clearTimer();
                killEvent = this.keyNavigation(aEvent);
                break;
              }
            // Alt+Down falls through to history popup toggling code
              
            case KeyEvent.DOM_VK_F4:
              if (!aEvent.ctrlKey && !aEvent.shiftKey && this.getAttribute("enablehistory") == "true") {
                var historyPopup = document.getAnonymousElementByAttribute(this, "anonid", "historydropmarker");
                if (historyPopup)
                  historyPopup.showPopup();
                else
                  historyPopup.hidePopup();
              }
              break;
            case KeyEvent.DOM_VK_PAGE_UP:
            case KeyEvent.DOM_VK_PAGE_DOWN:
            case KeyEvent.DOM_VK_UP:
              if (!aEvent.ctrlKey && !aEvent.metaKey) {
                this.clearTimer();
                killEvent = this.keyNavigation(aEvent);
              }
              break;
          }
          
          if (killEvent) {
            aEvent.preventDefault();
            aEvent.stopPropagation();
          }
          
          return true;
        ]]></body>
      </method>

      <!--  -->
      <method name="processStartComposition">
        <body><![CDATA[
          this.finishAutoComplete(false, false, null);
          this.clearTimer();
          this.closeResultPopup();
        ]]></body>
      </method>

      <!--  -->
      <method name="keyNavigation">
        <parameter name="aEvent"/>
        <body><![CDATA[
          var k = aEvent.keyCode;
          if (k == KeyEvent.DOM_VK_TAB ||
              k == KeyEvent.DOM_VK_UP || k == KeyEvent.DOM_VK_DOWN ||
              k == KeyEvent.DOM_VK_PAGE_UP || k == KeyEvent.DOM_VK_PAGE_DOWN)
          {
            if (!this.mMenuOpen) {
              // Original xpfe style was to allow the up and down keys to have
              // their default Mac action if the popup could not be opened.
              // For compatibility for toolkit we now have to predict which
              // keys have a default action that we can always allow to fire.
              if (/Mac/.test(navigator.platform) &&
                  ((k == KeyEvent.DOM_VK_UP &&
                   (this.selectionStart != 0 ||
                    this.selectionEnd != 0)) ||
                  (k == KeyEvent.DOM_VK_DOWN &&
                   (this.selectionStart != this.value.length ||
                    this.selectionEnd != this.value.length))))
                return false;
              if (this.currentSearchString != this.value) {
                 this.processInput();
                 return true;
              }
              if (this.view.rowCount < this.minResultsForPopup)
                return true; // used to be false, see above

              this.mNeedToFinish = true;
              this.openResultPopup();
              return true;
            }
            
            this.userAction = "scrolling";
            this.mNeedToComplete = false;
            
            var reverse = k == KeyEvent.DOM_VK_TAB && aEvent.shiftKey ||
                          k == KeyEvent.DOM_VK_UP ||
                          k == KeyEvent.DOM_VK_PAGE_UP;
            var page = k == KeyEvent.DOM_VK_PAGE_UP ||
                       k == KeyEvent.DOM_VK_PAGE_DOWN;
            var selected = this.resultsPopup.selectBy(reverse, page);
          
            // determine which value to place in the textbox
            this.ignoreInputEvent = true;
            if (selected != -1) {
              if (this.getSessionStatusAt(selected) == 
                  Components.interfaces.nsIAutoCompleteStatus.failureItems) { 
                if (this.currentSearchString)
                  this.setTextValue(this.currentSearchString);
              } else {
                this.setTextValue(this.getResultValueAt(selected));
              }
              this.mTransientValue = true;
            } else {
              if (this.currentSearchString)
                this.setTextValue(this.currentSearchString);
              this.mTransientValue = false;
            }

            // move cursor to the end
            this.mInputElt.setSelectionRange(this.value.length, this.value.length);
            this.ignoreInputEvent = false;
          }
          return true;
        ]]></body>
      </method>

      <!-- while the user is typing, fill the textbox with the "default" value
           if one can be assumed, and select the end of the text -->
      <method name="autoFillInput">
        <parameter name="aSessionName"/>
        <parameter name="aResults"/>
        <parameter name="aUseFirstMatchIfNoDefault"/>
        <body><![CDATA[
          if (this.mInputElt.selectionEnd < this.currentSearchString.length ||
              this.mDefaultMatchFilled)
            return;

          if (!this.mFinishAfterSearch &&
              (this.autoFill || this.completeDefaultIndex) && 
              this.mLastKeyCode != KeyEvent.DOM_VK_BACK_SPACE &&
              this.mLastKeyCode != KeyEvent.DOM_VK_DELETE) {
            var indexToUse = aResults.defaultItemIndex;
            if (aUseFirstMatchIfNoDefault && indexToUse == -1)
              indexToUse = 0; 

            if (indexToUse != -1) {
              var resultValue = this.getSessionValueAt(aSessionName, indexToUse);
              var match = resultValue.toLowerCase();
              var entry = this.currentSearchString.toLowerCase();
              this.ignoreInputEvent = true;
              if (match.indexOf(entry) == 0) {
                var endPoint = this.value.length;
                this.setTextValue(this.value + resultValue.substr(endPoint));
                this.mInputElt.setSelectionRange(endPoint, this.value.length);
              } else {
                if (this.completeDefaultIndex) {
                  this.setTextValue(this.value + " >> " + resultValue);
                  this.mInputElt.setSelectionRange(entry.length, this.value.length);
                } else {
                  var postIndex = resultValue.indexOf(this.value);
                  if (postIndex >= 0) {
                    var startPt = this.value.length;
                    this.setTextValue(this.value + 
                                      resultValue.substr(startPt+postIndex));
                    this.mInputElt.setSelectionRange(startPt, this.value.length);
                  }
                }
              }
              this.mNeedToComplete = true;
              this.ignoreInputEvent = false;
              this.mDefaultMatchFilled = true;
            }
          } 
        ]]></body>
      </method>

      <!-- ::::::::::::: popup and tree ::::::::::::: -->

      <!--  -->
      <method name="openResultPopup">
        <body><![CDATA[
          if (!this.mMenuOpen && this.focused &&
               (this.getResultCount() >= this.minResultsForPopup
                || this.mFailureItems)) {
            var w = this.boxObject.width;
            if (w != this.resultsPopup.boxObject.width)
              this.resultsPopup.setAttribute("width", w);
            this.resultsPopup.showPopup(this, -1, -1, "popup", "bottomleft", "topleft");
            this.mMenuOpen = true;
          }
        ]]></body>
      </method>

      <!--  -->
      <method name="closeResultPopup">
        <body><![CDATA[
          if (this.resultsPopup && this.mMenuOpen) {
            this.resultsPopup.hidePopup();
            this.mMenuOpen = false;
          }
        ]]></body>
      </method>

      <!--  -->
      <method name="addResultElements">
        <parameter name="aResults"/>
        <parameter name="aOldResults"/>
        <body><![CDATA[
          if (this.focused && this.showPopup) {
            this.view.replaceResults(aResults, aOldResults);
            this.resultsPopup.adjustHeight();
          }
        ]]></body>
      </method>

      <!--  -->
      <method name="clearResultElements">
        <parameter name="aInvalidate"/>
        <body><![CDATA[
          this.view.clearResults();
          if (aInvalidate)
            this.resultsPopup.adjustHeight();

          this.noMatch = true;
        ]]></body>
      </method>

      <!--  -->
      <method name="setTextValue">
        <parameter name="aValue"/>
        <body><![CDATA[
          // Completing a result should simulate the user typing the result,
          // so fire an input event.
          this.value = aValue;
          var evt = document.createEvent("UIEvents");
          evt.initUIEvent("input", true, false, window, 0);
          var oldIgnoreInput = this.ignoreInputEvent;
          this.ignoreInputEvent = true;
          this.dispatchEvent(evt);
          this.ignoreInputEvent = oldIgnoreInput;
        ]]></body>
      </method>

      <!--  -->
      <method name="clearResultData">
        <body><![CDATA[
          const nsIAutoCompleteItem =
            Components.interfaces.nsIAutoCompleteItem;
          for (var name in this.mSessions) {
            // clearing out mLastResults[name] does not guarantee that
            // each result will go away right now (it might be gc'ed later)
            // so we have to clear the 'param' element manually
            var session = this.mLastResults[name];
            if (session) {
              const resultCount = session.items.Count();
              for (var i=0; i<resultCount; i++)
                  session.items.QueryElementAt(i, nsIAutoCompleteItem).param = null;
              this.mLastResults[name] = null;
            }
            this.mLastStatus[name] = null;
          }
        ]]></body>
      </method>

      <!-- ::::::::::::: miscellaneous ::::::::::::: -->

      <!--  -->
      <method name="ifSetAttribute">
        <parameter name="aAttr"/>
        <parameter name="aVal"/>
        <body><![CDATA[
          if (!this.hasAttribute(aAttr))
            this.setAttribute(aAttr, aVal);
        ]]></body>
      </method>

      <!--  -->
      <method name="clearTimer">
        <parameter name=""/>
        <body><![CDATA[
          if (this.mAutoCompleteTimer) {
            clearTimeout(this.mAutoCompleteTimer);
            this.mAutoCompleteTimer = 0;
          }
        ]]></body>
      </method>

      <!-- ::::::::::::: event dispatching ::::::::::::: -->

      <method name="_fireEvent">
        <parameter name="aEventType"/>
        <parameter name="aEventParam"/>
        <parameter name="aTriggeringEvent"/>
        <body>
        <![CDATA[
          var noCancel = true;
          // handle any xml attribute event handlers
          var handler = this.getAttribute("on"+aEventType);
          if (handler) {
            var fn = new Function("eventParam", "domEvent", handler);
            var returned = fn.apply(this, [aEventParam, aTriggeringEvent]);
            if (returned == false)
              noCancel = false;
          }
          
          return noCancel;
        ]]>
        </body>
      </method>

      <!-- =================== TREE VIEW =================== -->

      <field name="view"><![CDATA[
        ({
          mTextbox: this,
          mTree: null,
          mSelection: null,
          mResults: [],
          mCounts: [],
          mRowCount: 0,
          
          clearResults: function()
          {
            var oldCount = this.mRowCount;
            this.mRowCount = 0;  
            this.mResults = [];
            this.mCounts = [];
            
            if (this.mTree) {
              this.mTree.rowCountChanged(0, -oldCount);
              this.mTree.scrollToRow(0);
            }
          },
          
          replaceResults: function(aResults, aOldResults)
          {
            var count = aResults.items.Count();
            var row = this.mRowCount;
            var oldIndex = this.mResults.indexOf(aOldResults);
            if (oldIndex == -1) {
              this.mResults.push(aResults);
              this.mCounts.push(count);
            } else {
              this.mResults[oldIndex] = aResults;
              count -= this.mCounts[oldIndex];
              this.mCounts[oldIndex] += count;
              while (++oldIndex < this.mCounts.length)
                row -= this.mCounts[oldIndex];
            }
            this.mRowCount += count;
            
            if (this.mTree)
              this.mTree.rowCountChanged(row, count);
          },
          
          createAtom: function(aVal)
          {
            try {
              var i = Components.interfaces.nsIAtomService;
              var svc = Components.classes["@mozilla.org/atom-service;1"].getService(i);
              return svc.getAtom(aVal);
            } catch(ex) { }
            return null; // XXX equivalent to falling off the end?
          },

          //////////////////////////////////////////////////////////
          // nsIAutoCompleteController interface

          // this is the only method required by the treebody mouseup handler
          handleEnter: function(aIsPopupSelection) {
            this.mTextbox.onResultClick();
          },

          //////////////////////////////////////////////////////////
          // nsITreeView interface
          
          get rowCount() {
            return this.mRowCount;
          },
          
          get selection() {
            return this.mSelection;
          },

          set selection(aVal) {
            return this.mSelection = aVal;
          },
          
          setTree: function(aTree)
          {
            this.mTree = aTree;
          },
          
          getCellText: function(aRow, aCol)
          {
            var result = this.mTextbox.getResultAt(aRow);
            if (!result) return "";
            return aCol.id == "treecolAutoCompleteValue" ? result.value : (aCol.id == "treecolAutoCompleteComment" ? result.comment : "");
          },

          getRowProperties: function(aIndex, aProperties) 
          {
          },

          getCellProperties: function(aIndex, aCol, aProperties)
          {
            // for the value column, append nsIAutoCompleteItem::className
            // to the property list so that we can style this column
            // using that property
            try { 
              if (aCol.id == "treecolAutoCompleteValue") {
                var className = this.mTextbox.getResultAt(aIndex).className;
                  if ( className ) {
                    aProperties.AppendElement(this.createAtom(className));
                  }
              }
            } catch (ex) {
              // the ability to style here is a frill, so don't abort
              // if there's a problem
            } 
          },

          getColumnProperties: function(aCol, aProperties) 
          {
          },
          
          getParentIndex: function(aRowIndex) { },
          hasNextSibling: function(aRowIndex, aAfterIndex) { },
          getLevel: function(aIndex) {},
          getImageSrc: function(aRow, aCol) {},
          getProgressMode: function(aRow, aCol) {},
          getCellValue: function(aRow, aCol) {},
          isContainer: function(aIndex) {},
          isContainerOpen: function(aIndex) {},
          isContainerEmpty: function(aIndex) {},
          isSeparator: function(aIndex) {},
          isSorted: function() {},
          toggleOpenState: function(aIndex) {},
          selectionChanged: function() {},
          cycleHeader: function(aCol) {},
          cycleCell: function(aRow, aCol) {},
          isEditable: function(aRow, aCol) {},
          isSelectable: function(aRow, aCol) {},
          setCellValue: function(aRow, aCol, aValue) {},
          setCellText: function(aRow, aCol, aValue) {},
          performAction: function(aAction) {},
          performActionOnRow: function(aAction, aRow) {},
          performActionOnCell: function(aAction, aRow, aCol) {}
        });
      ]]></field>

    </implementation>

    <handlers>
      <handler event="input"
               action="if (!this.ignoreInputEvent) this.processInput();"/>

      <handler event="keypress" phase="capturing"
                action="return this.processKeyPress(event);"/>

      <handler event="compositionstart" phase="capturing"
                action="this.processStartComposition();"/>

      <handler event="focus" phase="capturing"
               action="this.userAction = 'typing';"/>

      <handler event="blur" phase="capturing"
               action="if ( !(this.ignoreBlurWhileSearching &amp;&amp; this.isSearching) ) {this.userAction = 'none'; this.finishAutoComplete(false, false, event);}"/>

      <handler event="mousedown" phase="capturing"
               action="if ( !this.mMenuOpen ) this.finishAutoComplete(false, false, event);"/>
    </handlers>
  </binding> 

  <binding id="autocomplete-result-popup" extends="chrome://global/content/bindings/popup.xml#popup">
    <resources>
      <stylesheet src="chrome://global/content/autocomplete.css"/>
      <stylesheet src="chrome://global/skin/autocomplete.css"/>
    </resources>

    <content ignorekeys="true" level="top">
      <xul:tree anonid="tree" class="autocomplete-tree plain" flex="1">
        <xul:treecols anonid="treecols">
          <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteValue" flex="2"/>
          <xul:treecol class="autocomplete-treecol" id="treecolAutoCompleteComment" flex="1" hidden="true"/>
        </xul:treecols>
        <xul:treechildren anonid="treebody" class="autocomplete-treebody"/>
      </xul:tree>
    </content>
    
    <implementation implements="nsIAutoCompletePopup">
      <constructor><![CDATA[
        if (this.textbox && this.textbox.view)
          this.initialize();
      ]]></constructor>

      <destructor><![CDATA[
        if (this.view)
          this.tree.view = null;
      ]]></destructor>

      <field name="textbox">
        document.getBindingParent(this);
      </field>

      <field name="tree">
        document.getAnonymousElementByAttribute(this, "anonid", "tree");
      </field>

      <field name="treecols">
        document.getAnonymousElementByAttribute(this, "anonid", "treecols");
      </field>

      <field name="treebody">
        document.getAnonymousElementByAttribute(this, "anonid", "treebody");
      </field>

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

      <!-- Setting tree.view doesn't always immediately create a selection,
           so we ensure the selection by asking the tree for the view. Note:
           this.view.selection is quicker if we know the selection exists. -->
      <property name="selection" onget="return this.tree.view.selection;"/>

     <property name="pageCount"
               onget="return this.tree.treeBoxObject.getPageLength();"/>

     <field name="maxRows">0</field>
     <field name="mLastRows">0</field>

      <method name="initialize">
        <body><![CDATA[
        this.showCommentColumn = this.textbox.showCommentColumn;
        this.tree.view = this.textbox.view;
        this.view = this.textbox.view;
        this.maxRows = this.textbox.maxRows;
        ]]></body>
      </method>

      <property name="showCommentColumn"
                onget="return !this.treecols.lastChild.hidden;"
                onset="this.treecols.lastChild.hidden = !val; return val;"/>

      <method name="adjustHeight">
        <body><![CDATA[
          // detect the desired height of the tree
          var bx = this.tree.treeBoxObject;
          var view = this.view;
          var rows = this.maxRows || 6;
          if (!view.rowCount || (rows && view.rowCount < rows))
            rows = view.rowCount;
          
          var height = rows * bx.rowHeight;
          
          if (height == 0)
            this.tree.setAttribute("collapsed", "true");
          else {
            if (this.tree.hasAttribute("collapsed"))
              this.tree.removeAttribute("collapsed");
            this.tree.setAttribute("height", height);
          }
        ]]></body>
      </method>

      <method name="clearSelection">
        <body>
          this.selection.clearSelection();
        </body>
      </method>

      <method name="selectBy">
        <parameter name="aReverse"/>
        <parameter name="aPage"/>
        <body><![CDATA[
          try {
            return this.selectedIndex = this.getNextIndex(aReverse, aPage, this.selectedIndex, this.view.rowCount - 1);
          } catch (ex) {
            // do nothing - occasionally timer-related js errors happen here
            // e.g. "this.selectedIndex has no properties", when you type fast and hit a
            // navigation key before this popup has opened
            return -1;
          }
        ]]></body>
      </method>

      <method name="getNextIndex">
        <parameter name="aReverse"/>
        <parameter name="aPage"/>
        <parameter name="aIndex"/>
        <parameter name="aMaxRow"/>
        <body><![CDATA[
          if (aMaxRow < 0)
            return -1;

          if (aIndex == -1)
            return aReverse ? aMaxRow : 0;
          if (aIndex == (aReverse ? 0 : aMaxRow))
            return -1;

          var amount = aPage ? this.pageCount - 1 : 1;
          aIndex = aReverse ? aIndex - amount : aIndex + amount;
          if (aIndex > aMaxRow)
            return aMaxRow;
          if (aIndex < 0)
            return 0;
          return aIndex;
        ]]></body>
      </method>

      <!-- =================== nsIAutoCompletePopup =================== -->

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

      <!-- This property is meant to be overriden by bindings extending
           this one.  When the user selects an item from the list by
           hitting enter or clicking, this method can set the value
           of the textbox to a different value if it wants to. -->
      <property name="overrideValue" readonly="true" onget="return null;"/>

      <property name="selectedIndex">
        <getter>
          if (!this.view || !this.selection.count)
            return -1;
          var start = {}, end = {};
          this.view.selection.getRangeAt(0, start, end);
          return start.value;
        </getter>
        <setter>
          if (this.view) {
            this.selection.select(val);
            if (val >= 0) {
              this.view.selection.currentIndex = -1;
              this.tree.treeBoxObject.ensureRowIsVisible(val);
            }
          }
          return val;
        </setter>
      </property>

      <property name="popupOpen" onget="return !!this.input;" readonly="true"/>

      <method name="openAutocompletePopup">
        <parameter name="aInput"/>
        <parameter name="aElement"/>
        <body><![CDATA[
          if (!this.input) {
            this.tree.view = aInput.controller;
            this.view = this.tree.view;
            this.showCommentColumn = aInput.showCommentColumn;
            this.maxRows = aInput.maxRows;
            this.invalidate();

            var viewer = aElement
                .ownerDocument
                .defaultView
                .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                .getInterface(Components.interfaces.nsIWebNavigation)
                .QueryInterface(Components.interfaces.nsIDocShell)
                .contentViewer
                .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);
            var rect = aElement.getBoundingClientRect();
            var width = Math.round((rect.right - rect.left) * viewer.fullZoom);
            this.setAttribute("width", width > 100 ? width : 100);
            const nsIPopupBoxObject = Components.interfaces.nsIPopupBoxObject;
            this.popupBoxObject.setConsumeRollupEvent(aInput.consumeRollupEvent
              ? nsIPopupBoxObject.ROLLUP_CONSUME
              : nsIPopupBoxObject.ROLLUP_NO_CONSUME);
            this.openPopup(aElement, "after_start", 0, 0, false, false);
            if (this.state != "closed")
              this.input = aInput;
          }
        ]]></body>
      </method>

      <method name="closePopup">
        <body>
          this.hidePopup();
        </body>
      </method>

      <method name="invalidate">
        <body>
          if (this.view)
            this.adjustHeight();
          this.tree.treeBoxObject.invalidate();
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="popupshowing">
        if (this.textbox)
          this.textbox.mMenuOpen = true;
      </handler>

      <handler event="popuphiding">
        if (this.textbox)
          this.textbox.mMenuOpen = false;
        this.clearSelection();
        this.input = null;
      </handler>
    </handlers>
  </binding>

  <binding id="autocomplete-treebody">
    <implementation>
      <field name="resultsPopup">document.getBindingParent(this);</field>

      <field name="mLastMoveTime">Date.now()</field>
    </implementation>
    
    <handlers>
      <handler event="mouseout" action="this.resultsPopup.selectedIndex = -1;"/>

      <handler event="mouseup"><![CDATA[
        var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
        if (rc != -1) {
          this.resultsPopup.selectedIndex = rc;
          this.resultsPopup.view.handleEnter(true);
        }
      ]]></handler>

      <handler event="mousemove"><![CDATA[
        if (Date.now() - this.mLastMoveTime > 30) {
          var rc = this.parentNode.treeBoxObject.getRowAt(event.clientX, event.clientY);
          if (rc != -1 && rc != this.resultsPopup.selectedIndex) 
            this.resultsPopup.selectedIndex = rc;
          this.mLastMoveTime = Date.now();
        }
      ]]></handler>
    </handlers>
  </binding>

  <binding id="autocomplete-history-popup"
           extends="chrome://global/content/bindings/popup.xml#popup-scrollbars">
    <resources>
      <stylesheet src="chrome://global/content/autocomplete.css"/>
      <stylesheet src="chrome://global/skin/autocomplete.css"/>
    </resources>
    
    <implementation>
      <method name="removeOpenAttribute">
        <parameter name="parentNode"/>
        <body><![CDATA[
          parentNode.removeAttribute("open");
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="popuphiding"><![CDATA[
        setTimeout(this.removeOpenAttribute, 0, this.parentNode);
      ]]></handler>
    </handlers>
  </binding>

  <binding id="history-dropmarker" extends="chrome://global/content/bindings/general.xml#dropmarker">
      
    <implementation>
      <method name="showPopup">
        <body><![CDATA[
          var textbox = document.getBindingParent(this);
          var kids = textbox.getElementsByClassName("autocomplete-history-popup");
          if (kids.item(0) && textbox.getAttribute("open") != "true") { // Open history popup
            kids[0].showPopup(textbox, -1, -1, "popup", "bottomleft", "topleft");
            textbox.setAttribute("open", "true");
          }
        ]]></body>
      </method>
    </implementation>
    
    <handlers>
      <handler event="mousedown"><![CDATA[
        this.showPopup();
      ]]></handler>
    </handlers>
  </binding>

</bindings>

