The SuggestBox widget and its dependencies.
Review by: ecc
           knorton


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@855 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/MultiWordSuggestOracle.java b/user/src/com/google/gwt/user/client/ui/MultiWordSuggestOracle.java
new file mode 100644
index 0000000..4a065fa
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/MultiWordSuggestOracle.java
@@ -0,0 +1,382 @@
+/*

+ * Copyright 2007 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.user.client.ui;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.HashMap;

+import java.util.HashSet;

+import java.util.Iterator;

+import java.util.List;

+

+/**

+ * The default {@link com.google.gwt.user.client.ui.SuggestOracle}. The default

+ * oracle returns potential suggestions based on breaking the query into

+ * separate words and looking for matches. It also modifies the returned text to

+ * show which prefix matched the query term. The matching is case insensitive.

+ * All suggestions are sorted before being passed into a response.

+ * <p>

+ * Example Table

+ * </p>

+ * <p>

+ * <table width = "100%" border = "1">

+ * <tr>

+ * <td><b> All Suggestions </b> </td>

+ * <td><b>Query string</b> </td>

+ * <td><b>Matching Suggestions</b></td>

+ * </tr>

+ * <tr>

+ * <td> John Smith, Joe Brown, Jane Doe, Jane Smith, Bob Jones</td>

+ * <td> Jo</td>

+ * <td> John Smith, Joe Brown, Bob Jones</td>

+ * </tr>

+ * <tr>

+ * <td> John Smith, Joe Brown, Jane Doe, Jane Smith, Bob Jones</td>

+ * <td> Smith</td>

+ * <td> John Smith, Jane Smith</td>

+ * </tr>

+ * <tr>

+ * <td> Georgia, New York, California</td>

+ * <td> g</td>

+ * <td> Georgia</td>

+ * </tr>

+ * </table>

+ * </p>

+ */

+public final class MultiWordSuggestOracle extends SuggestOracle {

+

+  /**

+   * Suggestion class for {@link MultiWordSuggestOracle}.

+   */

+  protected static class MultiWordSuggestion implements Suggestion {

+    private final String value;

+    private final String displayString;

+

+    /**

+     * Constructor for <code>MultiWordSuggestion</code>.

+     * 

+     * @param value the value

+     * @param displayString the display string

+     */

+    public MultiWordSuggestion(String value, String displayString) {

+      this.value = value;

+      this.displayString = displayString;

+    }

+

+    public String getDisplayString() {

+      return displayString;

+    }

+

+    public Object getValue() {

+      return value;

+    }

+  }

+

+  private static final char WHITESPACE_CHAR = ' ';

+  private static final String WHITESPACE_STRING = " ";

+

+  /**

+   * Regular expression used to collapse all whitespace in a query string.

+   */

+  private static final String NORMALIZE_TO_SINGLE_WHITE_SPACE = "\\s+";

+

+  private static HTML convertMe = new HTML();

+

+  /**

+   * Associates substrings with words.

+   */

+  private final PrefixTree tree = new PrefixTree();

+

+  /**

+   * Associates individual words with candidates.

+   */

+  private HashMap toCandidates = new HashMap();

+

+  /**

+   * Associates candidates with their formatted suggestions.

+   */

+  private HashMap toRealSuggestions = new HashMap();

+

+  /**

+   * The whitespace masks used to prevent matching and replacing of the given

+   * substrings.

+   */

+  private char[] whitespaceChars;

+

+  /**

+   * Constructor for <code>MultiWordSuggestOracle</code>. This uses a space as

+   * the whitespace character.

+   * 

+   * @see #MultiWordSuggestOracle(String)

+   */

+  public MultiWordSuggestOracle() {

+    this(" ");

+  }

+

+  /**

+   * Constructor for <code>MultiWordSuggestOracle</code> which takes in a set

+   * of whitespace chars that filter its input.

+   * <p>

+   * Example: If <code>".,"</code> is passed in as whitespace, then the string

+   * "foo.bar" would match the queries "foo", "bar", "foo.bar", "foo...bar", and

+   * "foo, bar". If the empty string is used, then all characters are used in

+   * matching. For example, the query "bar" would match "bar", but not "foo

+   * bar".

+   * </p>

+   * 

+   * @param whitespaceChars the characters to treat as word separators

+   */

+  public MultiWordSuggestOracle(String whitespaceChars) {

+    this.whitespaceChars = new char[whitespaceChars.length()];

+    for (int i = 0; i < whitespaceChars.length(); i++) {

+      this.whitespaceChars[i] = whitespaceChars.charAt(i);

+    }

+  }

+

+  /**

+   * Adds a suggestion to the oracle. Each suggestion must be plain text.

+   * 

+   * @param suggestion the suggestion

+   */

+  public void add(String suggestion) {

+    String candidate = normalizeSuggestion(suggestion);

+    // candidates --> real suggestions.

+    toRealSuggestions.put(candidate, suggestion);

+

+    // word fragments --> candidates.

+    String[] words = candidate.split(WHITESPACE_STRING);

+    for (int i = 0; i < words.length; i++) {

+      String word = words[i];

+      tree.add(word);

+      HashSet l = (HashSet) toCandidates.get(word);

+      if (l == null) {

+        l = new HashSet();

+        toCandidates.put(word, l);

+      }

+      l.add(candidate);

+    }

+  }

+

+  /**

+   * Adds all suggestions specified. Each suggestion must be plain text.

+   * 

+   * @param collection the collection

+   */

+  public void addAll(Collection collection) {

+    Iterator suggestions = collection.iterator();

+    while (suggestions.hasNext()) {

+      add((String) suggestions.next());

+    }

+  }

+

+  public boolean isDisplayStringHTML() {

+    return true;

+  }

+

+  public void requestSuggestions(Request request, Callback callback) {

+    final List suggestions = computeItemsFor(request.getQuery(), request

+      .getLimit());

+    Response response = new Response(suggestions);

+    callback.onSuggestionsReady(request, response);

+  }

+

+  String escapeText(String escapeMe) {

+    convertMe.setText(escapeMe);

+    String escaped = convertMe.getHTML();

+    return escaped;

+  }

+

+  /**

+   * Compute the suggestions that are matches for a given query.

+   * 

+   * @param query search string

+   * @param limit limit

+   * @return matching suggestions

+   */

+  private List computeItemsFor(String query, int limit) {

+    query = normalizeSearch(query);

+

+    // Get candidates from search words.

+    List candidates = createCandidatesFromSearch(query, limit);

+

+    // Convert candidates to suggestions.

+    return convertToFormattedSuggestions(query, candidates);

+  }

+

+  /**

+   * Returns real suggestions with the given query in <code>strong</code> html

+   * font.

+   * 

+   * @param query query string

+   * @param candidates candidates

+   * @return real suggestions

+   */

+  private List convertToFormattedSuggestions(String query, List candidates) {

+    List suggestions = new ArrayList();

+

+    for (int i = 0; i < candidates.size(); i++) {

+      String candidate = (String) candidates.get(i);

+      int index = 0;

+      int cursor = 0;

+      // Use real suggestion for assembly.

+      String formattedSuggestion = (String) toRealSuggestions.get(candidate);

+

+      // Create strong search string.

+      StringBuffer accum = new StringBuffer();

+

+      while (true) {

+        index = candidate.indexOf(query, index);

+        if (index == -1) {

+          break;

+        }

+        int endIndex = index + query.length();

+        if (index == 0 || (WHITESPACE_CHAR == candidate.charAt(index - 1))) {

+          String part1 = escapeText(formattedSuggestion

+            .substring(cursor, index));

+          String part2 = escapeText(formattedSuggestion.substring(index,

+            endIndex));

+          cursor = endIndex;

+          accum.append(part1).append("<strong>").append(part2).append(

+            "</strong>");

+        }

+        index = endIndex;

+      }

+

+      // Check to make sure the search was found in the string.

+      if (cursor == 0) {

+        continue;

+      }

+

+      // Finish creating the formatted string.

+      String end = escapeText(formattedSuggestion.substring(cursor));

+      accum.append(end);

+      MultiWordSuggestion suggestion = new MultiWordSuggestion(

+        formattedSuggestion, accum.toString());

+      suggestions.add(suggestion);

+    }

+    return suggestions;

+  }

+

+  /**

+   * Find the sorted list of candidates that are matches for the given query.

+   */

+  private List createCandidatesFromSearch(String query, int limit) {

+    ArrayList candidates = new ArrayList();

+

+    if (query.length() == 0) {

+      return candidates;

+    }

+

+    // Find all words to search for.

+    String[] searchWords = query.split(WHITESPACE_STRING);

+    HashSet candidateSet = null;

+    for (int i = 0; i < searchWords.length; i++) {

+      String word = searchWords[i];

+

+      // Eliminate bogus word choices.

+      if (word.length() == 0 || word.matches(WHITESPACE_STRING)) {

+        continue;

+      }

+

+      // Find the set of candidates that are associated with all the

+      // searchWords.

+      HashSet thisWordChoices = createCandidatesFromWord(word);

+      if (candidateSet == null) {

+        candidateSet = thisWordChoices;

+      } else {

+        candidateSet.retainAll(thisWordChoices);

+

+        if (candidateSet.size() < 2) {

+          // If there is only one candidate, on average it is cheaper to

+          // check if that candidate contains our search string than to

+          // continue intersecting suggestion sets.

+          break;

+        }

+      }

+    }

+    if (candidateSet != null) {

+      candidates.addAll(candidateSet);

+      Collections.sort(candidates);

+      // Respect limit for number of choices.

+      for (int i = candidates.size() - 1; i > limit; i--) {

+        candidates.remove(i);

+      }

+    }

+    return candidates;

+  }

+

+  /**

+   * Creates a set of potential candidates that match the given query.

+   * 

+   * @param limit number of candidates to return

+   * @param query query string

+   * @return possible candidates

+   */

+  private HashSet createCandidatesFromWord(String query) {

+    HashSet candidateSet = new HashSet();

+    List words = tree.getSuggestions(query, Integer.MAX_VALUE);

+    if (words != null) {

+      // Find all candidates that contain the given word the search is a

+      // subset of.

+      for (int i = 0; i < words.size(); i++) {

+        Collection belongsTo = (Collection) toCandidates.get(words.get(i));

+        if (belongsTo != null) {

+          candidateSet.addAll(belongsTo);

+        }

+      }

+    }

+    return candidateSet;

+  }

+

+  /**

+   * Normalize the search key by making it lower case, removing multiple spaces,

+   * apply whitespace masks, and make it lower case.

+   */

+  private String normalizeSearch(String search) {

+    // Use the same whitespace masks and case normalization for the search

+    // string as was used with the candidate values.

+    search = normalizeSuggestion(search);

+

+    // Remove all excess whitespace from the search string.

+    search = search.replaceAll(NORMALIZE_TO_SINGLE_WHITE_SPACE,

+      WHITESPACE_STRING);

+

+    return search.trim();

+  }

+

+  /**

+   * Takes the formatted suggestion, makes it lower case and blanks out any

+   * existing whitespace for searching.

+   */

+  private String normalizeSuggestion(String formattedSuggestion) {

+    // Formatted suggestions should already have normalized whitespace. So we

+    // can skip that step.

+

+    // Lower case suggestion.

+    formattedSuggestion = formattedSuggestion.toLowerCase();

+

+    // Apply whitespace.

+    if (whitespaceChars != null) {

+      for (int i = 0; i < whitespaceChars.length; i++) {

+        char ignore = whitespaceChars[i];

+        formattedSuggestion = formattedSuggestion.replace(ignore,

+          WHITESPACE_CHAR);

+      }

+    }

+    return formattedSuggestion;

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/ui/SuggestBox.java b/user/src/com/google/gwt/user/client/ui/SuggestBox.java
new file mode 100644
index 0000000..d0e1fe6
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/SuggestBox.java
@@ -0,0 +1,411 @@
+/*

+ * Copyright 2007 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.user.client.ui;

+

+import com.google.gwt.user.client.ui.SuggestOracle.Callback;

+import com.google.gwt.user.client.ui.SuggestOracle.Request;

+import com.google.gwt.user.client.ui.SuggestOracle.Response;

+import com.google.gwt.user.client.ui.impl.ItemPickerDropDownImpl;

+import com.google.gwt.user.client.ui.impl.SuggestPickerImpl;

+

+import java.util.Collection;

+

+/**

+ * A {@link SuggestBox} is a text box or text area which displays a

+ * pre-configured set of selections that match the user's input.

+ * 

+ * Each {@link SuggestBox} is associated with a single {@link SuggestOracle}.

+ * The {@link SuggestOracle} is used to provide a set of selections given a

+ * specific query string.

+ * 

+ * <p>

+ * By default, the {@link SuggestBox} uses a {@link MultiWordSuggestOracle} as

+ * its oracle. Below we show how a {@link MultiWordSuggestOracle} can be

+ * configured:

+ * </p>

+ * 

+ * <pre> 

+ *   MultiWordSuggestOracle oracle = new MultiWordSuggestOracle();  

+ *   oracle.add("Cat");

+ *   oracle.add("Dog");

+ *   oracle.add("Horse");

+ *   oracle.add("Canary");

+ *   

+ *   SuggestBox box = new SuggestBox(oracle);

+ * </pre>

+ * 

+ * Using the example above, if the user types "C" into the text widget, the

+ * oracle will configure the suggestions with the "Cat" and "Canary"

+ * suggestions. Specifically, whenever the user types a key into the text

+ * widget, the value is submitted to the <code>MultiWordSuggestOracle</code>.

+ * 

+ * <p>

+ * <img class='gallery' src='SuggestBox.png'/>

+ * </p>

+ * 

+ * <h3>CSS Style Rules</h3>

+ * <ul class='css'>

+ * <li>.gwt-SuggestBox { the suggest box itself }</li>

+ * <li>.gwt-SuggestBoxPopup { the suggestion popup }</li>

+ * <li>.gwt-SuggestBoxPopup .item { an unselected suggestion }</li>

+ * <li>.gwt-SuggestBoxPopup .item-selected { a selected suggestion }</li>

+ * </ul>

+ * 

+ * @see SuggestOracle, MultiWordSuggestOracle, TextBoxBase

+ */

+public final class SuggestBox extends Composite implements HasText, HasFocus,

+    SourcesClickEvents, SourcesFocusEvents, SourcesChangeEvents,

+    SourcesKeyboardEvents {

+

+  private static final String STYLENAME_DEFAULT = "gwt-SuggestBox";

+

+  private int limit = 20;

+  private int selectStart;

+  private int selectEnd;

+  private SuggestOracle oracle;

+  private char[] separators;

+  private String currentValue;

+  private final PopupPanel popup;

+  private final SuggestPickerImpl picker;

+  private final TextBoxBase box;

+  private DelegatingClickListenerCollection clickListeners;

+  private DelegatingChangeListenerCollection changeListeners;

+  private DelegatingFocusListenerCollection focusListeners;

+  private DelegatingKeyboardListenerCollection keyboardListeners;

+  private String separatorPadding = "";

+

+  private final Callback callBack = new Callback() {

+    public void onSuggestionsReady(Request request, Response response) {

+      showSuggestions(response.getSuggestions());

+    }

+  };

+

+  /**

+   * Constructor for {@link SuggestBox}. Creates a

+   * {@link MultiWordSuggestOracle} and {@link TextBox} to use with this

+   * {@link SuggestBox}.

+   */

+  public SuggestBox() {

+    this(new MultiWordSuggestOracle());

+  }

+

+  /**

+   * Constructor for {@link SuggestBox}. Creates a {@link TextBox} to use with

+   * this {@link SuggestBox}.

+   * 

+   * @param oracle the oracle for this <code>SuggestBox</code>

+   */

+  public SuggestBox(SuggestOracle oracle) {

+    this(oracle, new TextBox());

+  }

+

+  /**

+   * Constructor for {@link SuggestBox}. The text box will be removed from it's

+   * current location and wrapped by the {@link SuggestBox}.

+   * 

+   * @param oracle supplies suggestions based upon the current contents of the

+   *          text widget

+   * @param box the text widget

+   */

+  public SuggestBox(SuggestOracle oracle, TextBoxBase box) {

+    this.box = box;

+    initWidget(box);

+    this.picker = new SuggestPickerImpl(oracle.isDisplayStringHTML());

+    this.popup = new ItemPickerDropDownImpl(this, picker);

+    addPopupChangeListener();

+    addKeyboardSupport();

+    setOracle(oracle);

+    setStyleName(STYLENAME_DEFAULT);

+  }

+

+  public final void addChangeListener(ChangeListener listener) {

+    if (changeListeners == null) {

+      changeListeners = new DelegatingChangeListenerCollection(this, box);

+    }

+    changeListeners.add(listener);

+  }

+

+  public final void addClickListener(ClickListener listener) {

+    if (clickListeners == null) {

+      clickListeners = new DelegatingClickListenerCollection(this, box);

+    }

+    clickListeners.add(listener);

+  }

+

+  public final void addFocusListener(FocusListener listener) {

+    if (focusListeners == null) {

+      focusListeners = new DelegatingFocusListenerCollection(this, box);

+    }

+    focusListeners.add(listener);

+  }

+

+  public final void addKeyboardListener(KeyboardListener listener) {

+    if (keyboardListeners == null) {

+      keyboardListeners = new DelegatingKeyboardListenerCollection(this, box);

+    }

+    keyboardListeners.add(listener);

+  }

+

+  /**

+   * Gets the limit for the number of suggestions that should be displayed for

+   * this box. It is up to the current {@link SuggestOracle} to enforce this

+   * limit.

+   * 

+   * @return the limit for the number of suggestions

+   */

+  public final int getLimit() {

+    return limit;

+  }

+

+  /**

+   * Gets the suggest box's {@link com.google.gwt.user.client.ui.SuggestOracle}.

+   * 

+   * @return the {@link SuggestOracle}

+   */

+  public final SuggestOracle getSuggestOracle() {

+    return oracle;

+  }

+

+  public final int getTabIndex() {

+    return box.getTabIndex();

+  }

+

+  public final String getText() {

+    return box.getText();

+  }

+

+  public final void removeChangeListener(ChangeListener listener) {

+    if (clickListeners != null) {

+      clickListeners.remove(listener);

+    }

+  }

+

+  public final void removeClickListener(ClickListener listener) {

+    if (clickListeners != null) {

+      clickListeners.remove(listener);

+    }

+  }

+

+  public final void removeFocusListener(FocusListener listener) {

+    if (focusListeners != null) {

+      focusListeners.remove(listener);

+    }

+  }

+

+  public final void removeKeyboardListener(KeyboardListener listener) {

+    if (keyboardListeners != null) {

+      keyboardListeners.remove(listener);

+    }

+  }

+

+  public final void setAccessKey(char key) {

+    box.setAccessKey(key);

+  }

+

+  public final void setFocus(boolean focused) {

+    box.setFocus(focused);

+  }

+

+  /**

+   * Sets the limit to the number of suggestions the oracle should provide. It

+   * is up to the oracle to enforce this limit.

+   * 

+   * @param limit the limit to the number of suggestions provided

+   */

+  public final void setLimit(int limit) {

+    this.limit = limit;

+  }

+

+  public final void setTabIndex(int index) {

+    box.setTabIndex(index);

+  }

+

+  public final void setText(String text) {

+    box.setText(text);

+  }

+

+  /**

+   * Show the given collection of suggestions.

+   * 

+   * @param suggestions suggestions to show

+   */

+  private void showSuggestions(Collection suggestions) {

+    if (suggestions.size() > 0) {

+      picker.setItems(suggestions);

+      popup.show();

+    } else {

+      popup.hide();

+    }

+  }

+

+  private void addKeyboardSupport() {

+    box.addKeyboardListener(new KeyboardListenerAdapter() {

+      private boolean pendingCancel;

+

+      public void onKeyDown(Widget sender, char keyCode, int modifiers) {

+        pendingCancel = picker.delegateKeyDown(keyCode);

+      }

+

+      public void onKeyPress(Widget sender, char keyCode, int modifiers) {

+        if (pendingCancel) {

+          // IE does not allow cancel key on key down, so we have delayed the

+          // cancellation of the key until the associated key press.

+          box.cancelKey();

+          pendingCancel = false;

+        } else if (popup.isAttached()) {

+          if (separators != null && isSeparator(keyCode)) {

+            // onKeyDown/onKeyUps's keyCode for ',' comes back '1/4', so unlike

+            // navigation, we use key press events to determine when the user

+            // wants to simulate clicking on the popup.

+            picker.commitSelection();

+

+            // The separator will be added after the popup is activated, so the

+            // popup will have already added a new separator. Therefore, the

+            // original separator should not be added as well.

+            box.cancelKey();

+          }

+        }

+      }

+

+      public void onKeyUp(Widget sender, char keyCode, int modifiers) {

+        // After every user key input, refresh the popup's suggestions.

+        refreshSuggestions();

+      }

+

+      /**

+       * In the presence of separators, returns the active search selection.

+       */

+      private String getActiveSelection(String text) {

+        selectEnd = box.getCursorPos();

+

+        // Find the last instance of a separator.

+        selectStart = -1;

+        for (int i = 0; i < separators.length; i++) {

+          selectStart = Math.max(

+              text.lastIndexOf(separators[i], selectEnd - 1), selectStart);

+        }

+        ++selectStart;

+

+        return text.substring(selectStart, selectEnd).trim();

+      }

+

+      private void refreshSuggestions() {

+        // Get the raw text.

+        String text = box.getText();

+        if (text.equals(currentValue)) {

+          return;

+        } else {

+          currentValue = text;

+        }

+

+        // Find selection to replace.

+        String selection;

+        if (separators == null) {

+          selection = text;

+        } else {

+          selection = getActiveSelection(text);

+        }

+        // If we have no text, let's not show the suggestions.

+        if (selection.length() == 0) {

+          popup.hide();

+        } else {

+          showSuggestions(selection);

+        }

+      }

+    });

+  }

+

+  /**

+   * Adds a standard popup listener to the suggest box's popup.

+   */

+  private void addPopupChangeListener() {

+    picker.addChangeListener(new ChangeListener() {

+      public void onChange(Widget sender) {

+        if (separators != null) {

+          onChangeWithSeparators();

+        } else {

+          currentValue = picker.getSelectedValue().toString();

+          box.setText(currentValue);

+        }

+        if (changeListeners != null) {

+          changeListeners.fireChange(SuggestBox.this);

+        }

+      }

+

+      private void onChangeWithSeparators() {

+        String newValue = (String) picker.getSelectedValue();

+

+        StringBuffer accum = new StringBuffer();

+        String text = box.getText();

+

+        // Add all text up to the selection start.

+        accum.append(text.substring(0, selectStart));

+

+        // Add one space if not at start.

+        if (selectStart > 0) {

+          accum.append(separatorPadding);

+        }

+        // Add the new value.

+        accum.append(newValue);

+

+        // Find correct cursor position.

+        int savedCursorPos = accum.length();

+

+        // Add all text after the selection end

+        String ender = text.substring(selectEnd).trim();

+        if (ender.length() == 0 || !isSeparator(ender.charAt(0))) {

+          // Add a separator if the first char of the ender is not already a

+          // separator.

+          accum.append(separators[0]).append(separatorPadding);

+          savedCursorPos = accum.length();

+        }

+        accum.append(ender);

+

+        // Set the text and cursor pos to correct location.

+        String replacement = accum.toString();

+        currentValue = replacement.trim();

+        box.setText(replacement);

+        box.setCursorPos(savedCursorPos);

+      }

+    });

+  }

+

+  /**

+   * Convenience method for identifying if a character is a separator.

+   */

+  private boolean isSeparator(char candidate) {

+    // An int map would be very handy right here...

+    for (int i = 0; i < separators.length; i++) {

+      if (candidate == separators[i]) {

+        return true;

+      }

+    }

+    return false;

+  }

+

+  /**

+   * Sets the suggestion oracle used to create suggestions.

+   * 

+   * @param oracle the oracle

+   */

+  private void setOracle(SuggestOracle oracle) {

+    this.oracle = oracle;

+  }

+

+  private void showSuggestions(String query) {

+    oracle.requestSuggestions(new Request(query, limit), callBack);

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/ui/SuggestOracle.java b/user/src/com/google/gwt/user/client/ui/SuggestOracle.java
new file mode 100644
index 0000000..db62d1f
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/SuggestOracle.java
@@ -0,0 +1,217 @@
+/*

+ * Copyright 2007 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.user.client.ui;

+

+import com.google.gwt.user.client.rpc.IsSerializable;

+

+import java.util.Collection;

+

+/**

+ * A {@link com.google.gwt.user.client.ui.SuggestOracle} can be used to create

+ * suggestions associated with a specific query string. It is currently used by

+ * {@link SuggestBox}.

+ * 

+ * @see SuggestBox

+ */

+public abstract class SuggestOracle {

+

+  /**

+   * Constructor for {@link com.google.gwt.user.client.ui.SuggestOracle}.

+   */

+  public SuggestOracle() {

+  }

+

+  /**

+   * Should {@link Suggestion} display strings be treated as HTML? If true, this

+   * all suggestions' display strings will be interpreted as HTML, otherwise as

+   * text.

+   * 

+   * @return by default, returns false

+   */

+  public boolean isDisplayStringHTML() {

+    return false;

+  }

+

+  /**

+   * Generate a {@link Response} based on a specific {@link Request}. After the

+   * {@link Response} is created, it is passed into

+   * {@link Callback#onSuggestionsReady(com.google.gwt.user.client.ui.SuggestOracle.Request, com.google.gwt.user.client.ui.SuggestOracle.Response)}.

+   * 

+   * @param request the request

+   * @param callback the callback to use for the response

+   */

+  public abstract void requestSuggestions(Request request, Callback callback);

+

+  /**

+   * Callback for {@link com.google.gwt.user.client.ui.SuggestOracle}. Every

+   * {@link Request} should be associated with a callback that should be called

+   * after a {@link  Response} is generated.

+   */

+  public interface Callback {

+    /**

+     * Consume the suggestions created by a

+     * {@link com.google.gwt.user.client.ui.SuggestOracle} in response to a

+     * {@link Request}.

+     * 

+     * @param request the request

+     * @param response the response

+     */

+    public void onSuggestionsReady(Request request, Response response);

+  }

+

+  /**

+   * A {@link com.google.gwt.user.client.ui.SuggestOracle} request.

+   */

+  public static class Request implements IsSerializable {

+    private int limit = 20;

+    private String query;

+

+    /**

+     * Constructor for {@link Request}.

+     */

+    public Request() {

+    }

+

+    /**

+     * Constructor for {@link Request}.

+     * 

+     * @param query the query string

+     */

+    public Request(String query) {

+      setQuery(query);

+    }

+

+    /**

+     * Constructor for {@link Request}.

+     * 

+     * @param query the query string

+     * @param limit limit on the number of suggestions that should be created

+     *          for this query

+     */

+    public Request(String query, int limit) {

+      setQuery(query);

+      setLimit(limit);

+    }

+

+    /**

+     * Gets the limit on the number of suggestions that should be created.

+     * 

+     * @return the limit

+     */

+    public int getLimit() {

+      return limit;

+    }

+

+    /**

+     * Gets the query string.

+     * 

+     * @return the query string

+     */

+    public String getQuery() {

+      return query;

+    }

+

+    /**

+     * Sets the limit on the number of suggestions that should be created.

+     * 

+     * @param limit the limit

+     */

+    public void setLimit(int limit) {

+      this.limit = limit;

+    }

+

+    /**

+     * Sets the query string used for this request.

+     * 

+     * @param query the query string

+     */

+    public void setQuery(String query) {

+      this.query = query;

+    }

+  }

+

+  /**

+   * {@link com.google.gwt.user.client.ui.SuggestOracle} response.

+   */

+  public static class Response implements IsSerializable {

+

+    /**

+     * @gwt.typeArgs <com.google.gwt.user.client.ui.SuggestOracle.Suggestion>

+     */

+    private Collection suggestions;

+

+    /**

+     * Constructor for {@link Response}.

+     */

+    public Response() {

+    }

+

+    /**

+     * Constructor for {@link Response}.

+     * 

+     * @param suggestions each element of suggestions must implement the

+     *          {@link Suggestion} interface

+     */

+    public Response(Collection suggestions) {

+      setSuggestions(suggestions);

+    }

+

+    /**

+     * Gets the collection of suggestions. Each suggestion must implement the

+     * {@link Suggestion} interface.

+     * 

+     * @return the collection of suggestions

+     */

+    public Collection getSuggestions() {

+      return this.suggestions;

+    }

+

+    /**

+     * Sets the suggestions for this response. Each suggestion must implement

+     * the {@link Suggestion} interface.

+     * 

+     * @param suggestions the suggestions

+     */

+    public void setSuggestions(Collection suggestions) {

+      this.suggestions = suggestions;

+    }

+  }

+

+  /**

+   * Suggestion supplied by the

+   * {@link com.google.gwt.user.client.ui.SuggestOracle}. Each suggestion has a

+   * value and a display string. The interpretation of the display string

+   * depends upon the value of its oracle's {@link SuggestOracle#isDisplayStringHTML()}.

+   * 

+   */

+  public interface Suggestion {

+    /**

+     * Gets the display string associated with this suggestion. The

+     * interpretation of the display string depends upon the value of

+     * its oracle's {@link SuggestOracle#isDisplayStringHTML()}.

+     * 

+     * @return the display string

+     */

+    String getDisplayString();

+

+    /**

+     * Get the value associated with this suggestion.

+     * 

+     * @return the value

+     */

+    Object getValue();

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/ui/impl/AbstractItemPickerImpl.java b/user/src/com/google/gwt/user/client/ui/impl/AbstractItemPickerImpl.java
index 82ae5aa..73ae298 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/AbstractItemPickerImpl.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/AbstractItemPickerImpl.java
@@ -76,7 +76,7 @@
     }

   }

 

-  private static final String STYLENAME_SELECTED_ITEM = "selected";

+  private static final String STYLENAME_SELECTED_ITEM = "item-selected";

   private static final String STYLENAME_ITEM = "item";

 

   final Element body;

diff --git a/user/src/com/google/gwt/user/client/ui/impl/ItemPickerDropDownImpl.java b/user/src/com/google/gwt/user/client/ui/impl/ItemPickerDropDownImpl.java
new file mode 100644
index 0000000..cccfe29
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/ItemPickerDropDownImpl.java
@@ -0,0 +1,96 @@
+/*

+ * Copyright 2007 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.user.client.ui.impl;

+

+import com.google.gwt.user.client.Window;

+import com.google.gwt.user.client.ui.ChangeListener;

+import com.google.gwt.user.client.ui.HasFocus;

+import com.google.gwt.user.client.ui.PopupPanel;

+import com.google.gwt.user.client.ui.UIObject;

+import com.google.gwt.user.client.ui.Widget;

+

+/**

+ * Suggestion picker drop-down, used in the implementation of

+ * {@link com.google.gwt.user.client.ui.SuggestBox}.

+ */

+public class ItemPickerDropDownImpl extends PopupPanel {

+  private final AbstractItemPickerImpl picker;

+  private final HasFocus owner;

+

+  public ItemPickerDropDownImpl(final HasFocus owner, AbstractItemPickerImpl picker) {

+    super(true);

+    setWidget(picker);

+    this.picker = picker;

+    this.owner = owner;

+

+    picker.addChangeListener(new ChangeListener() {

+      public void onChange(Widget sender) {

+        hide();

+      }

+    });

+  }

+

+  /**

+   * Shows the popup, by default <code>show</code> selects the first item and

+   * displays itself under it's owner.

+   */

+  public void show() {

+    showBelow((UIObject) owner);

+  }

+

+  /**

+   * Shows the popup below the given UI object. By default, first item is

+   * selected in the item picker.

+   * <p>

+   * Note, if the popup would not be visible on the browser, than the popup's

+   * position may be adjusted.

+   * </p>

+   * 

+   * @param showBelow the <code>UIObject</code> beneath which the popup should

+   *          be shown

+   */

+  public void showBelow(UIObject showBelow) {

+    // A drop down with 0 items should never show itself.

+    if (picker.getItemCount() == 0) {

+      hide();

+      return;

+    }

+

+    // Initialize the picker to the first element.

+    picker.setSelectedIndex(0);

+

+    // Show must be called first, as otherwise getOffsetWidth is not correct. As

+    // the adjustment is very fast, the user experience is not effected by this

+    // call.

+    super.show();

+

+    // Calculate left.

+    int left = showBelow.getAbsoluteLeft();

+    int windowRight = Window.getClientWidth() + Window.getScrollLeft();

+    int overshootLeft = Math.max(0, (left + getOffsetWidth()) - windowRight);

+    left = left - overshootLeft;

+

+    // Calculate top.

+    int top = showBelow.getAbsoluteTop() + showBelow.getOffsetHeight();

+    int windowBottom = Window.getScrollTop() + Window.getClientHeight();

+    int overshootTop = Math.max(0, (top + getOffsetHeight()) - windowBottom);

+    top = top - overshootTop;

+

+    // Set the popup position.

+    setPopupPosition(left, top);

+    super.show();

+  }

+}

diff --git a/user/src/com/google/gwt/user/client/ui/impl/SuggestPickerImpl.java b/user/src/com/google/gwt/user/client/ui/impl/SuggestPickerImpl.java
new file mode 100644
index 0000000..ccf936f
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/impl/SuggestPickerImpl.java
@@ -0,0 +1,160 @@
+/*

+ * Copyright 2007 Google Inc.

+ * 

+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

+ * 

+ * Unless required by applicable law or agreed to in writing, software

+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT

+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the

+ * License for the specific language governing permissions and limitations under

+ * the License.

+ */

+package com.google.gwt.user.client.ui.impl;

+

+import com.google.gwt.user.client.DOM;

+import com.google.gwt.user.client.Element;

+import com.google.gwt.user.client.ui.KeyboardListener;

+import com.google.gwt.user.client.ui.UIObject;

+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;

+

+import java.util.Collection;

+import java.util.Iterator;

+

+/**

+ * Suggestion picker, used in the implementation of

+ * {@link com.google.gwt.user.client.ui.SuggestBox}.

+ */

+public class SuggestPickerImpl extends AbstractItemPickerImpl {

+

+  /**

+   * Default style for the picker.

+   */

+  private static final String STYLENAME_DEFAULT = "gwt-SuggestBoxPopup";

+  private final boolean asHTML;

+  private int startInvisible = Integer.MAX_VALUE;

+

+  /**

+   * Constructor for <code>SuggestPickerImpl</code>.

+   * 

+   * @param asHTML flag used to indicate how to treat {@link Suggestion} display

+   *          strings

+   */

+  public SuggestPickerImpl(boolean asHTML) {

+    this.asHTML = asHTML;

+    setStyleName(STYLENAME_DEFAULT);

+  }

+

+  public boolean delegateKeyDown(char keyCode) {

+    if (isAttached()) {

+      switch (keyCode) {

+        case KeyboardListener.KEY_DOWN:

+          shiftSelection(1);

+          return true;

+        case KeyboardListener.KEY_UP:

+          shiftSelection(-1);

+          return true;

+        case KeyboardListener.KEY_ENTER:

+          commitSelection();

+          return true;

+      }

+    }

+    return false;

+  }

+

+  public int getItemCount() {

+    if (startInvisible == Integer.MAX_VALUE) {

+      return 0;

+    } else {

+      return startInvisible;

+    }

+  }

+

+  /**

+   * Sets the suggestions associated with this picker.

+   * 

+   * @param suggestions suggestions for this picker

+   */

+  public final void setItems(Collection suggestions) {

+    setItems(suggestions.iterator());

+  }

+

+  protected native Element getRow(Element elem, int row)/*-{

+   return elem.rows[row];

+   }-*/;

+

+  void shiftSelection(int shift) {

+    int newSelect = getSelectedIndex() + shift;

+    if (newSelect >= super.getItemCount() || newSelect < 0

+      || newSelect >= startInvisible) {

+      return;

+    }

+    setSelection(getItem(newSelect));

+  }

+

+  /**

+   * Ensures the existence of the given item and returns it.

+   * 

+   * @param itemIndex item index to ensure

+   * @return associated item

+   */

+  private Item ensureItem(int itemIndex) {

+    for (int i = super.getItemCount(); i <= itemIndex; i++) {

+      Item item = new Item(i);

+      addItem(item, true);

+    }

+    return getItem(itemIndex);

+  }

+

+  /**

+   * Sets the suggestions associated with this picker.

+   */

+  private final void setItems(Iterator suggestions) {

+    int itemCount = 0;

+

+    // Ensure all needed items exist and set each item's html to the given

+    // suggestion.

+    while (suggestions.hasNext()) {

+      Item item = ensureItem(itemCount);

+      Suggestion suggestion = (Suggestion) suggestions.next();

+      String display = suggestion.getDisplayString();

+      if (asHTML) {

+        DOM.setInnerHTML(item.getElement(), display);

+      } else {

+        DOM.setInnerText(item.getElement(), display);

+      }

+      item.setValue(suggestion.getValue());

+      ++itemCount;

+    }

+

+    if (itemCount == 0) {

+      throw new IllegalStateException(

+        "Must set at least one item in a SuggestPicker");

+    }

+

+    // Render visible all needed cells.

+    int min = Math.min(itemCount, super.getItemCount());

+    for (int i = startInvisible; i < min; i++) {

+      setVisible(i, true);

+    }

+

+    // Render invisible all useless cells.

+    startInvisible = itemCount;

+    for (int i = itemCount; i < super.getItemCount(); i++) {

+      setVisible(i, false);

+    }

+  }

+

+  /**

+   * Sets whether the given item is visible.

+   * 

+   * @param itemIndex item index

+   * @param visible visible boolean

+   */

+  private void setVisible(int itemIndex, boolean visible) {

+    UIObject.setVisible(getRow(body, itemIndex), visible);

+  }

+}