<?xml version="1.0"?>
<!DOCTYPE window [
<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd" >
%textcontextDTD;
]>

<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">

  <binding id="autocomplete" extends="chrome://global/content/xulBindings.xml#textbox">
    <content context="_child">
      <xul:box class="textbox-internal-box" flex="1">
        <html:input class="textbox-input" flex="1" inherits="onfocus,onblur,value,type,maxlength,disabled,size,readonly"/>
        <xul:popupset ignorekeys="true" oncommand="var me = this.parentNode.parentNode; me.privatefunc.onMenuCommand(me, this);">
          <xul:popup class="textbox-popup" oncreate="this.parentNode.parentNode.parentNode.menuOpen='true'" ondestroy="this.parentNode.parentNode.parentNode.menuOpen='false';"/>
        </xul:popupset>
      </xul:box>
      <xul:popupset>
        <xul:popup oncreate="this.parentNode.parentNode.doPopupItemEnabling(this); this.focus();">
          <xul:menuitem label="&undoCmd.label;" accesskey="&undoCmd.accesskey;" cmd="cmd_undo"
                        oncommand="var controller = document.commandDispatcher.getControllerForCommand('cmd_undo');              
                                   controller.doCommand('cmd_undo');"/>
          <xul:menuseparator/>
          <xul:menuitem label="&cutCmd.label;" accesskey="&cutCmd.accesskey;" cmd="cmd_cut"
                        oncommand="var controller = document.commandDispatcher.getControllerForCommand('cmd_cut');
                                   controller.doCommand('cmd_cut');"/>
          <xul:menuitem label="&copyCmd.label;" accesskey="&copyCmd.accesskey;" cmd="cmd_copy"
                        oncommand="var controller = document.commandDispatcher.getControllerForCommand('cmd_copy');
                                   controller.doCommand('cmd_copy');"/>
          <xul:menuitem label="&pasteCmd.label;" accesskey="&pasteCmd.accesskey;" cmd="cmd_paste"
                        oncommand="var controller = document.commandDispatcher.getControllerForCommand('cmd_paste');
                        controller.doCommand('cmd_paste');"/>
          <xul:menuitem label="&deleteCmd.label;" accesskey="&deleteCmd.accesskey;" cmd="cmd_delete"
                        oncommand="var controller = document.commandDispatcher.getControllerForCommand('cmd_delete');
                                   controller.doCommand('cmd_delete');"/>
          <xul:menuseparator/>
          <xul:menuitem label="&selectAllCmd.label;" accesskey="&selectAllCmd.accesskey;" cmd="cmd_selectAll"
                        oncommand="var controller = document.commandDispatcher.getControllerForCommand('cmd_selectAll');
                                   controller.doCommand('cmd_selectAll');"/>
        </xul:popup>
      </xul:popupset>
    </content>

    <implementation>
      <property name="value"
                onset="if (this.ignoreInputEventTimer)
                         clearTimeout(this.ignoreInputEventTimer);
                       this.ignoreInputEvent = true;
                       this.ignoreInputEventTimer = setTimeout(this.privatefunc.resetInput, 250, this);
                       return document.getAnonymousNodes(this)[0].firstChild.value = val;"
                onget="this.privatefunc.cleanupInputField(this); return document.getAnonymousNodes(this)[0].firstChild.value;"/>

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

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

      <property name="autoCompleteSession">
        <![CDATA[
          if (this.getAttribute('searchSessionType') != "") {
            var searchSession = unescape('@mozilla%2Eorg%2FautocompleteSession%3B1%3Ftype=');
            searchSession = searchSession + this.getAttribute('searchSessionType');
            try {
              Components.classes[searchSession].getService(Components.interfaces.nsIAutoCompleteSession);
              // var session = Components.classes[searchSession].createInstance();
              // session.QueryInterface(Components.interfaces.nsIAutoCompleteSession);
            }
            catch (e) {
              dump("### ERROR, cannot create a search session. " + e + "\n");
            }
          }
        ]]>
      </property>
        
      <property name="disableAutocomplete">false</property>
      <property name="autoCompleteTimer">0</property>
      <property name="ignoreInputEventTimer">0</property>

      <property name="lastResults">
        <![CDATA[
          var results = Components.classes["@mozilla.org/autocomplete/results;1"].createInstance();
          results.QueryInterface(Components.interfaces.nsIAutoCompleteResults);
        ]]>
      </property>

      <property name="lastKeyPressed">0</property>
      <property name="noDirectMatch">0</property>
      <property name="menuOpen">0</property>

      <property name="autoCompleteListener">
        <![CDATA[
          ({
            onAutoComplete: function(result, status) {
              var me = this.param;
              if (status == Components.interfaces.nsIAutoCompleteStatus.failed)
                return;
              
              if (me.disableAutocomplete)
                return;

              me.lastResults = result;
              if (status == Components.interfaces.nsIAutoCompleteStatus.ignored || 
                  status == Components.interfaces.nsIAutoCompleteStatus.noMatch) {
                return;
              }
                
              if (result == null && result.items.Count() == 0)
                return;

              if (result.defaultItemIndex > result.items.Count())
                result.defaultItemIndex = 0;
                
              // Do not alter the user input when deleting characters
              if (me.lastKeyPressed == 8 /*vk_back*/ || me.lastKeyPressed == 46 /*vk_delete*/)
                result.defaultItemIndex = -1;

              var inputElement = document.getAnonymousNodes(me)[0].firstChild;
                
              // Time to build the new edit field value
              // First, check if the search string correspond to the current value of the field, else ignore it
              if (result.searchString != inputElement.value)
                return;

              var item = null;
              if (result.defaultItemIndex != -1) {
                item = result.items.QueryElementAt(result.defaultItemIndex, Components.interfaces.nsIAutoCompleteItem);
                var match = item.value.toLowerCase();
                var entry = inputElement.value.toLowerCase();
                if (entry != match) {
                  me.ignoreInputEvent = true;
                  if (match.substring(0, entry.length) == entry) {
                    inputElement.value = inputElement.value + item.value.substring(entry.length, match.length);
                    inputElement.setSelectionRange(entry.length, match.length);
                    me.noDirectMatch = false;
                  }
                  else {
                    inputElement.value = inputElement.value + " >> " + item.value;
                    inputElement.setSelectionRange(entry.length, inputElement.value.length);
                    me.noDirectMatch = true;
                  }
                  me.ignoreInputEvent = false;
                }
              }

              // Now, build the popup content
              if (me.displayMenu == "false")
                return;

              var popupset = document.getAnonymousNodes(me)[0].childNodes[1];
              var popupElement = popupset.firstChild;

              // First, remove all the current menu items
              for (i = popupElement.childNodes.length - 1; i >= 0 ; i--)
                popupElement.removeChild(popupElement.childNodes[i]);

              // Then build the new menu items
              for (var i = 0; i < result.items.Count(); i ++) {
                item = result.items.QueryElementAt(i, Components.interfaces.nsIAutoCompleteItem);
                var menuitem = document.createElement("menuitem");
                menuitem.setAttribute('value', i);
                menuitem.setAttribute('label', item.value);
                popupElement.appendChild(menuitem);
                // dump("  match=" + item.value + "\n");
              }
              // dump("  count=" + result.items.Count() + ", default=" + result.defaultItemIndex + "\n");
      
              me.privatefunc.selectedItemIndex = result.defaultItemIndex;
              if (result.defaultItemIndex != 0 || result.items.Count() != 1) {
                // me.privatefunc.closePopupMenu(me); //Close it first as openPopup seems to work as a toggle!
                popupset.firstChild.openPopup(document.getAnonymousNodes(me)[0].firstChild, -1, -1, "popup", "bottomleft", "topleft");

                if (result.defaultItemIndex != -1)
                  popupElement.activeChild = popupElement.childNodes[result.defaultItemIndex];
              }
            },
            param: this
          })
        ]]>
      </property>

      <property name="privatefunc">
        <![CDATA[
          ({
            onMenuCommand: function(me, popupSetElem) {
              var inputElement = document.getAnonymousNodes(me)[0].firstChild;
              var popupElem = popupSetElem.firstChild;

              me.ignoreInputEvent = true;

              inputElement.value = popupElem.activeChild.getAttribute("label");
              inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);

              needToAutocomplete = false;
              me.privatefunc.selectedItemIndex = popupElem.activeChild.getAttribute("value");

              me.ignoreInputEvent = false;

              return;
            },

            callListener: function(me, action) {
              switch (action) {
                case 'startLookup':
                  if (me.disableAutocomplete)
                    return;
                  inputElement = document.getAnonymousNodes(me)[0].firstChild;
                  if (!me.lastResults || inputElement.value != me.lastResults.searchString)
                    me.autoCompleteSession.onStartLookup(inputElement.value, me.lastResults, me.autoCompleteListener);
                  break;
 
                case 'stopLookup':
                  me.autoCompleteSession.onStopLookup();
                  break;

                case 'autoComplete':
                  if (me.autoCompleteTimer) {
                    clearTimeout(me.autoCompleteTimer);
                    me.autoCompleteTimer = 0;
                  }
                  me.needToAutocomplete = false;
                  if (me.disableAutocomplete)
                    return;
                  var inputElement = document.getAnonymousNodes(me)[0].firstChild;
                  me.autoCompleteSession.onAutoComplete(inputElement.value, me.lastResults, me.autoCompleteListener);
                  break;
              }
            },

            finishAutoComplete: function(me, event) {
              me.privatefunc.closePopupMenu(me);
              if (me.disableAutocomplete)
                  return;

              var inputElement = document.getAnonymousNodes(me)[0].firstChild;
              var value = inputElement.value;
              var entry = value.substring(0, inputElement.selectionStart) + value.substring(inputElement.selectionEnd, value.length);

              if (me.lastResults) {
                if (me.lastResults.searchString == entry) {
                  me.ignoreInputEvent = true;
                  try {
                    inputElement.value = me.lastResults.items.QueryElementAt(me.lastResults.defaultItemIndex, Components.interfaces.nsIAutoCompleteItem).value;
                  }
                  catch(e) {
                  }
                  inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);
                  me.ignoreInputEvent = false;
                  return;
                }
              }
                
              me.privatefunc.callListener(me, 'autoComplete');
            },

            closePopupMenu: function(me) {
              var popup = document.getAnonymousNodes(me)[0].childNodes[1].firstChild;
              if (popup && me.menuOpen == "true")
             popup.closePopup();
            },

            cleanupInputField: function(me) {
              if (me.noDirectMatch) {
                var inputElement = document.getAnonymousNodes(me)[0].firstChild;
                var value = inputElement.value;
                var index = value.indexOf(">> ");
                if (index >= 0) {
                  me.ignoreInputEvent = true;
                  inputElement.value = value.substr(index + 3);
                  inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);
                  me.ignoreInputEvent = false;
                }
              }
            },

            keyNavigation: function(me, event, popup) {
              if (me.lastResults == null)
                return;

              var inputElement = document.getAnonymousNodes(me)[0].firstChild;
              if (event.keyCode == 38 || event.keyCode == 40) {
                if (event.keyCode == 38) {
                  if (me.privatefunc.selectedItemIndex <= -1)
                    me.privatefunc.selectedItemIndex = me.lastResults.items.Count() - 1;
                  else
                    me.privatefunc.selectedItemIndex --;
                }
                else {
                  me.privatefunc.selectedItemIndex ++;
                  if (me.privatefunc.selectedItemIndex >= me.lastResults.items.Count())
                    me.privatefunc.selectedItemIndex = -1
                }

                me.ignoreInputEvent = true;
                if (me.privatefunc.selectedItemIndex == -1)
                  inputElement.value = me.lastResults.searchString;
                else
                  inputElement.value = me.lastResults.items.QueryElementAt(me.privatefunc.selectedItemIndex, Components.interfaces.nsIAutoCompleteItem).value;
                inputElement.setSelectionRange(inputElement.value.length, inputElement.value.length);
                noDirectMatch = false;
                needToAutocomplete = false;
                 me.ignoreInputEvent = false;

                if (popup) {
                  if (me.privatefunc.selectedItemIndex >= 0)
                    popup.activeChild = popup.childNodes[me.privatefunc.selectedItemIndex];
                  else
                    popup.activeChild = null;
                }
                return;
              }
            },

            resetInput: function(me) {
              me.ignoreInputEventTimer = null;
              me.ignoreInputEvent = false;
            },
        
            processInput: function(me) {
              // Stop current lookup in case it's async.
              me.privatefunc.callListener(me, 'stopLookup');
              if (me.autoCompleteTimer) {
                clearTimeout(me.autoCompleteTimer);
                me.autoCompleteTimer = 0;
              }

              if (me.ignoreInputEvent)
                return;
          
              me.privatefunc.closePopupMenu(me);
              me.lastKeyPressed = 0;

              if (me.disableAutocomplete)
                return;

              // We want to autocomplete only if the user is editing at the end of the text
              var inputElement = document.getAnonymousNodes(me)[0].firstChild;
              if (inputElement.selectionEnd >= inputElement.value.length) {
                me.needToAutocomplete = true;
                me.autoCompleteTimer = setTimeout(me.privatefunc.callListener, me.timeout, me, 'startLookup');
              }
            },

            processKeyPress: function(me, event) {
              if (me.disableAutocomplete) {
                me.privatefunc.closePopupMenu(me);
                return;
              }

              var popup = document.getAnonymousNodes(me)[0].childNodes[1].firstChild;
              if (popup && me.menuOpen != "true")
                popup = null;

              me.lastKeyPressed = event.keyCode;
              switch (event.keyCode) {
                case 9: /*vk_tab*/
                  if (popup)
                    me.privatefunc.closePopupMenu(me);
                  return;
  
                case 13 /*vk_return*/:
                  if (me.autoCompleteTimer) {
                    clearTimeout(me.autoCompleteTimer);
                    me.autoCompleteTimer = 0;
                  }
                  me.privatefunc.closePopupMenu(me);
                  me.privatefunc.finishAutoComplete(me, event);
                  return;
  
                case 27 /*vk_escape*/:
                  if (me.autoCompleteTimer) {
                    clearTimeout(me.autoCompleteTimer);
                    me.autoCompleteTimer = 0;
                  }
                  if (popup) {
                    me.privatefunc.closePopupMenu(me);
                    event.preventDefault();
                    event.preventBubble();
                    return;
                  }
                  break;
    
                case 37 /*vk_left*/:
                case 39 /*vk_right*/:
                  if (me.autoCompleteTimer) {
                    clearTimeout(me.autoCompleteTimer);
                    me.autoCompleteTimer = 0;
                  }
                  if (popup) {
                    me.privatefunc.closePopupMenu(me);
                    event.preventDefault();
                    event.preventBubble();
                  }
                  me.privatefunc.cleanupInputField(me);
                  break;
    
                case 38 /*vk_up*/:
                case 40 /*vk_down*/:
                  if (me.autoCompleteTimer) {
                    clearTimeout(me.autoCompleteTimer);
                    me.autoCompleteTimer = 0;
                  }
                  me.privatefunc.keyNavigation(me, event, popup);
                  event.preventDefault();
                  event.preventBubble();
                  if (!popup)
                    me.privatefunc.cleanupInputField(me);
                  break;
              }
            },
            selectedItemIndex: 0
          })
        ]]>
      </property>
    </implementation>

    <handlers>
      <handler event="click" action="this.privatefunc.cleanupInputField(this);"/>
      <handler event="dblclick" action="this.privatefunc.cleanupInputField(this);"/>
      <handler event="input" action="this.privatefunc.processInput(this);"/>
      <handler event="keypress" action="this.privatefunc.processKeyPress(this, event);"/>
      <handler event="focus" action="this.needToAutocomplete = false; this.lastResults.searchString=''; this.ignoreInputEvent = false"/>
      <handler event="blur" action="this.privatefunc.closePopupMenu(this);
                                    if (this.needToAutocomplete)
                                      this.privatefunc.finishAutoComplete(this, event);
                                    this.privatefunc.cleanupInputField(this);"/>
    </handlers>
  </binding> 
</bindings> 
