Adding SuggestionDisplay interface to SuggestBox to allow custom display implementations.

Patch by: jlabanca
Review by: jgw



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7400 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/SuggestBox.java b/user/src/com/google/gwt/user/client/ui/SuggestBox.java
index b18ba72..060cf8e 100644
--- a/user/src/com/google/gwt/user/client/ui/SuggestBox.java
+++ b/user/src/com/google/gwt/user/client/ui/SuggestBox.java
@@ -92,48 +92,6 @@
  * <dl>
  * <dt>.gwt-SuggestBox</dt>
  * <dd>the suggest box itself</dd>
- * <dt>.gwt-SuggestBoxPopup</dt>
- * <dd>the suggestion popup</dd>
- * <dt>.gwt-SuggestBoxPopup .item</dt>
- * <dd>an unselected suggestion</dd>
- * <dt>.gwt-SuggestBoxPopup .item-selected</dt>
- * <dd>a selected suggestion</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupTopLeft</dt>
- * <dd>the top left cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupTopLeftInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupTopCenter</dt>
- * <dd>the top center cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupTopCenterInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupTopRight</dt>
- * <dd>the top right cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupTopRightInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleLeft</dt>
- * <dd>the middle left cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleLeftInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleCenter</dt>
- * <dd>the middle center cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleCenterInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleRight</dt>
- * <dd>the middle right cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleRightInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomLeft</dt>
- * <dd>the bottom left cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomLeftInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomCenter</dt>
- * <dd>the bottom center cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomCenterInner</dt>
- * <dd>the inner element of the cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomRight</dt>
- * <dd>the bottom right cell</dd>
- * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomRightInner</dt>
- * <dd>the inner element of the cell</dd>
  * </dl>
  * 
  * @see SuggestOracle
@@ -147,6 +105,364 @@
     HasValue<String>, HasSelectionHandlers<Suggestion> {
 
   /**
+   * The callback used when a user selects a {@link Suggestion}.
+   */
+  public static interface SuggestionCallback {
+    void onSuggestionSelected(Suggestion suggestion);
+  }
+
+  /**
+   * Used to display suggestions to the user.
+   */
+  public abstract static class SuggestionDisplay {
+
+    /**
+     * Get the currently selected {@link Suggestion} in the display.
+     * 
+     * @return the current suggestion, or null if none selected
+     */
+    protected abstract Suggestion getCurrentSelection();
+
+    /**
+     * Hide the list of suggestions from view.
+     */
+    protected abstract void hideSuggestions();
+
+    /**
+     * Highlight the suggestion directly below the current selection in the
+     * list.
+     */
+    protected abstract void moveSelectionDown();
+
+    /**
+     * Highlight the suggestion directly above the current selection in the
+     * list.
+     */
+    protected abstract void moveSelectionUp();
+
+    /**
+     * Set the debug id of widgets used in the SuggestionDisplay.
+     * 
+     * @param suggestBoxBaseID the baseID of the {@link SuggestBox}
+     * @see UIObject#onEnsureDebugId(String)
+     */
+    protected void onEnsureDebugId(String suggestBoxBaseID) {
+    }
+
+    /**
+     * Update the list of visible suggestions.
+     * 
+     * @param suggestBox the suggest box where the suggestions originated
+     * @param suggestions the suggestions to show
+     * @param isDisplayStringHTML should the suggestions be displayed as HTML
+     * @param isAutoSelectEnabled if true, the first item should be selected
+     *          automatically
+     * @param callback the callback used when the user makes a suggestion
+     */
+    protected abstract void showSuggestions(SuggestBox suggestBox,
+        Collection<? extends Suggestion> suggestions,
+        boolean isDisplayStringHTML, boolean isAutoSelectEnabled,
+        SuggestionCallback callback);
+
+    /**
+     * This is here for legacy reasons. It is intentionally not visible.
+     * 
+     * @deprecated implemented in DefaultSuggestionDisplay
+     */
+    @Deprecated
+    boolean isAnimationEnabledImpl() {
+      // Implemented in DefaultSuggestionDisplay.
+      return false;
+    }
+
+    /**
+     * This is here for legacy reasons. It is intentionally not visible.
+     * 
+     * @deprecated implemented in DefaultSuggestionDisplay
+     */
+    @Deprecated
+    boolean isSuggestionListShowingImpl() {
+      // Implemented in DefaultSuggestionDisplay.
+      return false;
+    }
+
+    /**
+     * This is here for legacy reasons. It is intentionally not visible.
+     * 
+     * @deprecated implemented in DefaultSuggestionDisplay
+     */
+    @Deprecated
+    void setAnimationEnabledImpl(boolean enable) {
+      // Implemented in DefaultSuggestionDisplay.
+    }
+
+    /**
+     * This is here for legacy reasons. It is intentionally not visible.
+     * 
+     * @deprecated implemented in DefaultSuggestionDisplay
+     */
+    @Deprecated
+    void setPopupStyleNameImpl(String style) {
+      // Implemented in DefaultSuggestionDisplay.
+    }
+  }
+
+  /**
+   * <p>
+   * The default implementation of {@link SuggestionDisplay} displays
+   * suggestions in a {@link PopupPanel} beneath the {@link SuggestBox}.
+   * </p>
+   * 
+   * <h3>CSS Style Rules</h3>
+   * <dl>
+   * <dt>.gwt-SuggestBoxPopup</dt>
+   * <dd>the suggestion popup</dd>
+   * <dt>.gwt-SuggestBoxPopup .item</dt>
+   * <dd>an unselected suggestion</dd>
+   * <dt>.gwt-SuggestBoxPopup .item-selected</dt>
+   * <dd>a selected suggestion</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupTopLeft</dt>
+   * <dd>the top left cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupTopLeftInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupTopCenter</dt>
+   * <dd>the top center cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupTopCenterInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupTopRight</dt>
+   * <dd>the top right cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupTopRightInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleLeft</dt>
+   * <dd>the middle left cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleLeftInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleCenter</dt>
+   * <dd>the middle center cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleCenterInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleRight</dt>
+   * <dd>the middle right cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupMiddleRightInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomLeft</dt>
+   * <dd>the bottom left cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomLeftInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomCenter</dt>
+   * <dd>the bottom center cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomCenterInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomRight</dt>
+   * <dd>the bottom right cell</dd>
+   * <dt>.gwt-SuggestBoxPopup .suggestPopupBottomRightInner</dt>
+   * <dd>the inner element of the cell</dd>
+   * </dl>
+   */
+  public static class DefaultSuggestionDisplay extends SuggestionDisplay
+      implements HasAnimation {
+
+    private final SuggestionMenu suggestionMenu;
+    private final PopupPanel suggestionPopup;
+
+    /**
+     * We need to keep track of the last {@link SuggestBox} because it acts as
+     * an autoHide partner for the {@link PopupPanel}. If we use the same
+     * display for multiple {@link SuggestBox}, we need to switch the autoHide
+     * partner.
+     */
+    private SuggestBox lastSuggestBox = null;
+
+    /**
+     * Construct a new {@link DefaultSuggestionDisplay}.
+     */
+    public DefaultSuggestionDisplay() {
+      suggestionMenu = new SuggestionMenu(true);
+      suggestionPopup = createPopup();
+      suggestionPopup.setWidget(decorateSuggestionList(suggestionMenu));
+    }
+
+    @Override
+    public void hideSuggestions() {
+      suggestionPopup.hide();
+    }
+
+    public boolean isAnimationEnabled() {
+      return suggestionPopup.isAnimationEnabled();
+    }
+
+    /**
+     * Check whether or not the list of suggestions is being shown.
+     * 
+     * @return true if the suggestions are visible, false if not
+     */
+    public boolean isSuggestionListShowing() {
+      return suggestionPopup.isShowing();
+    }
+
+    public void setAnimationEnabled(boolean enable) {
+      suggestionPopup.setAnimationEnabled(enable);
+    }
+
+    /**
+     * Sets the style name of the suggestion popup.
+     * 
+     * @param style the new primary style name
+     * @see UIObject#setStyleName(String)
+     */
+    public void setPopupStyleName(String style) {
+      suggestionPopup.setStyleName(style);
+    }
+
+    /**
+     * Create the PopupPanel that will hold the list of suggestions.
+     * 
+     * @return the popup panel
+     */
+    protected PopupPanel createPopup() {
+      PopupPanel p = new DecoratedPopupPanel(true, false, "suggestPopup");
+      p.setStyleName("gwt-SuggestBoxPopup");
+      p.setPreviewingAllNativeEvents(true);
+      p.setAnimationType(AnimationType.ROLL_DOWN);
+      return p;
+    }
+
+    /**
+     * Wrap the list of suggestions before adding it to the popup. You can
+     * override this method if you want to wrap the suggestion list in a
+     * decorator.
+     * 
+     * @param suggestionList the widget that contains the list of suggestions
+     * @return the suggestList, optionally inside of a wrapper
+     */
+    protected Widget decorateSuggestionList(Widget suggestionList) {
+      return suggestionList;
+    }
+
+    @Override
+    protected Suggestion getCurrentSelection() {
+      if (!isSuggestionListShowing()) {
+        return null;
+      }
+      MenuItem item = suggestionMenu.getSelectedItem();
+      return item == null ? null : ((SuggestionMenuItem) item).getSuggestion();
+    }
+
+    /**
+     * Get the {@link PopupPanel} used to display suggestions.
+     * 
+     * @return the popup panel
+     */
+    protected PopupPanel getPopupPanel() {
+      return suggestionPopup;
+    }
+
+    @Override
+    protected void moveSelectionDown() {
+      // Make sure that the menu is actually showing. These keystrokes
+      // are only relevant when choosing a suggestion.
+      if (isSuggestionListShowing()) {
+        suggestionMenu.selectItem(suggestionMenu.getSelectedItemIndex() + 1);
+      }
+    }
+
+    @Override
+    protected void moveSelectionUp() {
+      // Make sure that the menu is actually showing. These keystrokes
+      // are only relevant when choosing a suggestion.
+      if (isSuggestionListShowing()) {
+        suggestionMenu.selectItem(suggestionMenu.getSelectedItemIndex() - 1);
+      }
+    }
+
+    /**
+     * <b>Affected Elements:</b>
+     * <ul>
+     * <li>-popup = The popup that appears with suggestions.</li>
+     * <li>-item# = The suggested item at the specified index.</li>
+     * </ul>
+     * 
+     * @see UIObject#onEnsureDebugId(String)
+     */
+    @Override
+    protected void onEnsureDebugId(String baseID) {
+      suggestionPopup.ensureDebugId(baseID + "-popup");
+      suggestionMenu.setMenuItemDebugIds(baseID);
+    }
+
+    @Override
+    protected void showSuggestions(final SuggestBox suggestBox,
+        Collection<? extends Suggestion> suggestions,
+        boolean isDisplayStringHTML, boolean isAutoSelectEnabled,
+        final SuggestionCallback callback) {
+      // Hide the popup if there are no suggestions to display.
+      if (suggestions == null || suggestions.size() == 0) {
+        hideSuggestions();
+        return;
+      }
+
+      // Hide the popup before we manipulate the menu within it. If we do not
+      // do this, some browsers will redraw the popup as items are removed
+      // and added to the menu.
+      if (suggestionPopup.isAttached()) {
+        suggestionPopup.hide();
+      }
+
+      suggestionMenu.clearItems();
+
+      for (final Suggestion curSuggestion : suggestions) {
+        final SuggestionMenuItem menuItem = new SuggestionMenuItem(
+            curSuggestion, isDisplayStringHTML);
+        menuItem.setCommand(new Command() {
+          public void execute() {
+            callback.onSuggestionSelected(curSuggestion);
+          }
+        });
+
+        suggestionMenu.addItem(menuItem);
+      }
+
+      if (isAutoSelectEnabled) {
+        // Select the first item in the suggestion menu.
+        suggestionMenu.selectItem(0);
+      }
+
+      // Link the popup autoHide to the TextBox.
+      if (lastSuggestBox != suggestBox) {
+        // If the suggest box has changed, free the old one first.
+        if (lastSuggestBox != null) {
+          suggestionPopup.removeAutoHidePartner(lastSuggestBox.getElement());
+        }
+        lastSuggestBox = suggestBox;
+        suggestionPopup.addAutoHidePartner(suggestBox.getElement());
+      }
+
+      // Show the popup under the TextBox.
+      suggestionPopup.showRelativeTo(suggestBox);
+    }
+
+    @Override
+    boolean isAnimationEnabledImpl() {
+      return isAnimationEnabled();
+    }
+
+    @Override
+    boolean isSuggestionListShowingImpl() {
+      return isSuggestionListShowing();
+    }
+
+    @Override
+    void setAnimationEnabledImpl(boolean enable) {
+      setAnimationEnabled(enable);
+    }
+
+    @Override
+    void setPopupStyleNameImpl(String style) {
+      setPopupStyleName(style);
+    }
+  }
+
+  /**
    * The SuggestionMenu class is used for the display and selection of
    * suggestions in the SuggestBox widget. SuggestionMenu differs from MenuBar
    * in that it always has a vertical orientation, and it has no submenus. It
@@ -273,12 +589,18 @@
   private boolean selectsFirstItem = true;
   private SuggestOracle oracle;
   private String currentText;
-  private final SuggestionMenu suggestionMenu;
-  private final PopupPanel suggestionPopup;
+  private final SuggestionDisplay display;
   private final TextBoxBase box;
   private final Callback callback = new Callback() {
     public void onSuggestionsReady(Request request, Response response) {
-      showSuggestions(response.getSuggestions());
+      display.showSuggestions(SuggestBox.this, response.getSuggestions(),
+          oracle.isDisplayStringHTML(), isAutoSelectEnabled(),
+          suggestionCallback);
+    }
+  };
+  private final SuggestionCallback suggestionCallback = new SuggestionCallback() {
+    public void onSuggestionSelected(Suggestion suggestion) {
+      setNewSelection(suggestion);
     }
   };
 
@@ -310,14 +632,23 @@
    * @param box the text widget
    */
   public SuggestBox(SuggestOracle oracle, TextBoxBase box) {
-    this.box = box;
-    initWidget(box);
+    this(oracle, box, new DefaultSuggestionDisplay());
+  }
 
-    // suggestionMenu must be created before suggestionPopup, because
-    // suggestionMenu is suggestionPopup's widget
-    suggestionMenu = new SuggestionMenu(true);
-    suggestionPopup = createPopup();
-    suggestionPopup.setAnimationType(AnimationType.ROLL_DOWN);
+  /**
+   * 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
+   * @param suggestDisplay the class used to display suggestions
+   */
+  public SuggestBox(SuggestOracle oracle, TextBoxBase box,
+      SuggestionDisplay suggestDisplay) {
+    this.box = box;
+    this.display = suggestDisplay;
+    initWidget(box);
 
     addEventsToTextBox();
 
@@ -335,7 +666,8 @@
    */
   @Deprecated
   public void addChangeListener(final ChangeListener listener) {
-    ListenerWrapper.WrappedLogicalChangeListener.add(box, listener).setSource(this);
+    ListenerWrapper.WrappedLogicalChangeListener.add(box, listener).setSource(
+        this);
   }
 
   /**
@@ -347,8 +679,8 @@
    */
   @Deprecated
   public void addClickListener(final ClickListener listener) {
-    ListenerWrapper.WrappedClickListener legacy = ListenerWrapper.WrappedClickListener.add(box,
-        listener);
+    ListenerWrapper.WrappedClickListener legacy = ListenerWrapper.WrappedClickListener.add(
+        box, listener);
     legacy.setSource(this);
   }
 
@@ -367,18 +699,19 @@
    * source Widget for these events will be the SuggestBox.
    * 
    * @param listener the listener interface to add
-   * @deprecated use {@link #getTextBox}().addFocusHandler/addBlurHandler() instead
+   * @deprecated use {@link #getTextBox}().addFocusHandler/addBlurHandler()
+   *             instead
    */
   @Deprecated
   public void addFocusListener(final FocusListener listener) {
-    ListenerWrapper.WrappedFocusListener focus = ListenerWrapper.WrappedFocusListener.add(box,
-        listener);
+    ListenerWrapper.WrappedFocusListener focus = ListenerWrapper.WrappedFocusListener.add(
+        box, listener);
     focus.setSource(this);
   }
 
   /**
-   * @deprecated Use {@link #addKeyDownHandler}, {@link
-   * #addKeyUpHandler} and {@link #addKeyPressHandler} instead
+   * @deprecated Use {@link #addKeyDownHandler}, {@link #addKeyUpHandler} and
+   *             {@link #addKeyPressHandler} instead
    */
   @Deprecated
   public void addKeyboardListener(KeyboardListener listener) {
@@ -419,6 +752,15 @@
   }
 
   /**
+   * Get the {@link SuggestionDisplay} used to display suggestions.
+   * 
+   * @return the {@link SuggestionDisplay}
+   */
+  public SuggestionDisplay getSuggestionDisplay() {
+    return display;
+  }
+
+  /**
    * Gets the suggest box's {@link com.google.gwt.user.client.ui.SuggestOracle}.
    * 
    * @return the {@link SuggestOracle}
@@ -449,14 +791,27 @@
   }
 
   /**
-   * Hide current suggestions.
+   * Hide current suggestions in the {@link DefaultSuggestionDisplay}. Note that
+   * this method is a no-op unless the {@link DefaultSuggestionDisplay} is used.
+   * 
+   * @deprecated use {@link DefaultSuggestionDisplay#hideSuggestions()} instead
    */
+  @Deprecated
   public void hideSuggestionList() {
-    this.suggestionPopup.hide();
+    display.hideSuggestions();
   }
 
+  /**
+   * Check whether or not the {@link DefaultSuggestionDisplay} has animations
+   * enabled. Note that this method only has a meaningful return value when the
+   * {@link DefaultSuggestionDisplay} is used.
+   * 
+   * @deprecated use {@link DefaultSuggestionDisplay#isAnimationEnabled()}
+   *             instead
+   */
+  @Deprecated
   public boolean isAnimationEnabled() {
-    return suggestionPopup.isAnimationEnabled();
+    return display.isAnimationEnabledImpl();
   }
 
   /**
@@ -470,15 +825,22 @@
   }
 
   /**
+   * Check if the {@link DefaultSuggestionDisplay} is showing. Note that this
+   * method only has a meaningful return value when the
+   * {@link DefaultSuggestionDisplay} is used.
+   * 
    * @return true if the list of suggestions is currently showing, false if not
+   * @deprecated use {@link DefaultSuggestionDisplay#isSuggestionListShowing()}
    */
+  @Deprecated
   public boolean isSuggestionListShowing() {
-    return suggestionPopup.isShowing();
+    return display.isSuggestionListShowingImpl();
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by {@link #getTextBox}().addChangeHandler instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by {@link #getTextBox}().addChangeHandler
+   *             instead
    */
   @Deprecated
   public void removeChangeListener(ChangeListener listener) {
@@ -486,8 +848,9 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by {@link #getTextBox}().addClickHandler instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by {@link #getTextBox}().addClickHandler
+   *             instead
    */
   @Deprecated
   public void removeClickListener(ClickListener listener) {
@@ -495,8 +858,8 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method no the object returned by {@link #addSelectionHandler} instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method no the
+   *             object returned by {@link #addSelectionHandler} instead
    */
   @Deprecated
   public void removeEventHandler(SuggestionHandler handler) {
@@ -504,8 +867,9 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by {@link #getTextBox}().addFocusListener instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by {@link #getTextBox}().addFocusListener
+   *             instead
    */
   @Deprecated
   public void removeFocusListener(FocusListener listener) {
@@ -513,8 +877,8 @@
   }
 
   /**
-   * @deprecated Use the {@link HandlerRegistration#removeHandler}
-   * method on the object returned by {@link #getTextBox}().add*Handler instead
+   * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the
+   *             object returned by {@link #getTextBox}().add*Handler instead
    */
   @Deprecated
   public void removeKeyboardListener(KeyboardListener listener) {
@@ -525,8 +889,18 @@
     box.setAccessKey(key);
   }
 
+  /**
+   * Enable or disable animations in the {@link DefaultSuggestionDisplay}. Note
+   * that this method is a no-op unless the {@link DefaultSuggestionDisplay} is
+   * used.
+   * 
+   * @deprecated use
+   *             {@link DefaultSuggestionDisplay#setAnimationEnabled(boolean)}
+   *             instead
+   */
+  @Deprecated
   public void setAnimationEnabled(boolean enable) {
-    suggestionPopup.setAnimationEnabled(enable);
+    display.setAnimationEnabledImpl(enable);
   }
 
   /**
@@ -555,13 +929,18 @@
   }
 
   /**
-   * Sets the style name of the suggestion popup.
+   * Sets the style name of the suggestion popup in the
+   * {@link DefaultSuggestionDisplay}. Note that this method is a no-op unless
+   * the {@link DefaultSuggestionDisplay} is used.
    * 
    * @param style the new primary style name
    * @see UIObject#setStyleName(String)
+   * @deprecated use {@link DefaultSuggestionDisplay#setPopupStyleName(String)}
+   *             instead
    */
+  @Deprecated
   public void setPopupStyleName(String style) {
-    suggestionPopup.setStyleName(style);
+    getSuggestionDisplay().setPopupStyleNameImpl(style);
   }
 
   public void setTabIndex(int index) {
@@ -590,47 +969,10 @@
     }
   }
 
-  /**
-   * <b>Affected Elements:</b>
-   * <ul>
-   * <li>-popup = The popup that appears with suggestions.</li>
-   * <li>-items-item# = The suggested item at the specified index.</li>
-   * </ul>
-   * 
-   * @see UIObject#onEnsureDebugId(String)
-   */
   @Override
   protected void onEnsureDebugId(String baseID) {
     super.onEnsureDebugId(baseID);
-    suggestionPopup.ensureDebugId(baseID + "-popup");
-    suggestionMenu.setMenuItemDebugIds(baseID);
-  }
-
-  /**
-   * Gets the specified suggestion from the suggestions currently showing.
-   * 
-   * @param index the index at which the suggestion lives
-   * 
-   * @throws IndexOutOfBoundsException if the index is greater then the number
-   *           of suggestions currently showing
-   * 
-   * @return the given suggestion
-   */
-  Suggestion getSuggestion(int index) {
-    if (!isSuggestionListShowing()) {
-      throw new IndexOutOfBoundsException(
-          "No suggestions showing, so cannot show " + index);
-    }
-    return ((SuggestionMenuItem) suggestionMenu.getItems().get(index)).suggestion;
-  }
-
-  /**
-   * Get the number of suggestions that are currently showing.
-   * 
-   * @return the number of suggestions currently showing, 0 if there are none
-   */
-  int getSuggestionCount() {
-    return isSuggestionListShowing() ? suggestionMenu.getNumItems() : 0;
+    display.onEnsureDebugId(baseID);
   }
 
   void showSuggestions(String query) {
@@ -646,25 +988,22 @@
         ValueChangeHandler<String> {
 
       public void onKeyDown(KeyDownEvent event) {
-        // Make sure that the menu is actually showing. These keystrokes
-        // are only relevant when choosing a suggestion.
-        if (suggestionPopup.isAttached()) {
-          switch (event.getNativeKeyCode()) {
-            case KeyCodes.KEY_DOWN:
-              suggestionMenu.selectItem(suggestionMenu.getSelectedItemIndex() + 1);
-              break;
-            case KeyCodes.KEY_UP:
-              suggestionMenu.selectItem(suggestionMenu.getSelectedItemIndex() - 1);
-              break;
-            case KeyCodes.KEY_ENTER:
-            case KeyCodes.KEY_TAB:
-              if (suggestionMenu.getSelectedItemIndex() < 0) {
-                suggestionPopup.hide();
-              } else {
-                suggestionMenu.doSelectedItemAction();
-              }
-              break;
-          }
+        switch (event.getNativeKeyCode()) {
+          case KeyCodes.KEY_DOWN:
+            display.moveSelectionDown();
+            break;
+          case KeyCodes.KEY_UP:
+            display.moveSelectionUp();
+            break;
+          case KeyCodes.KEY_ENTER:
+          case KeyCodes.KEY_TAB:
+            Suggestion suggestion = display.getCurrentSelection();
+            if (suggestion == null) {
+              display.hideSuggestions();
+            } else {
+              setNewSelection(suggestion);
+            }
+            break;
         }
         delegateEvent(SuggestBox.this, event);
       }
@@ -689,15 +1028,6 @@
     box.addValueChangeHandler(events);
   }
 
-  private PopupPanel createPopup() {
-    PopupPanel p = new DecoratedPopupPanel(true, false, "suggestPopup");
-    p.setWidget(suggestionMenu);
-    p.setStyleName("gwt-SuggestBoxPopup");
-    p.setPreviewingAllNativeEvents(true);
-    p.addAutoHidePartner(getTextBox().getElement());
-    return p;
-  }
-
   private void fireSuggestionEvent(Suggestion selectedSuggestion) {
     SelectionEvent.fire(this, selectedSuggestion);
   }
@@ -713,11 +1043,16 @@
     showSuggestions(text);
   }
 
-  private void setNewSelection(SuggestionMenuItem menuItem) {
-    Suggestion curSuggestion = menuItem.getSuggestion();
+  /**
+   * Set the new suggestion in the text box.
+   * 
+   * @param curSuggestion the new suggestion
+   */
+  private void setNewSelection(Suggestion curSuggestion) {
+    assert curSuggestion != null : "suggestion cannot be null";
     currentText = curSuggestion.getReplacementString();
     setText(currentText);
-    suggestionPopup.hide();
+    display.hideSuggestions();
     fireSuggestionEvent(curSuggestion);
   }
 
@@ -729,46 +1064,4 @@
   private void setOracle(SuggestOracle oracle) {
     this.oracle = oracle;
   }
-
-  /**
-   * Show the given collection of suggestions.
-   * 
-   * @param suggestions suggestions to show
-   */
-  private void showSuggestions(Collection<? extends Suggestion> suggestions) {
-    if (suggestions.size() > 0) {
-
-      // Hide the popup before we manipulate the menu within it. If we do not
-      // do this, some browsers will redraw the popup as items are removed
-      // and added to the menu.
-      boolean isAnimationEnabled = suggestionPopup.isAnimationEnabled();
-      if (suggestionPopup.isAttached()) {
-        suggestionPopup.hide();
-      }
-
-      suggestionMenu.clearItems();
-
-      for (Suggestion curSuggestion : suggestions) {
-        final SuggestionMenuItem menuItem = new SuggestionMenuItem(
-            curSuggestion, oracle.isDisplayStringHTML());
-        menuItem.setCommand(new Command() {
-          public void execute() {
-            SuggestBox.this.setNewSelection(menuItem);
-          }
-        });
-
-        suggestionMenu.addItem(menuItem);
-      }
-
-      if (selectsFirstItem) {
-        // Select the first item in the suggestion menu.
-        suggestionMenu.selectItem(0);
-      }
-
-      suggestionPopup.showRelativeTo(getTextBox());
-      suggestionPopup.setAnimationEnabled(isAnimationEnabled);
-    } else {
-      suggestionPopup.hide();
-    }
-  }
 }
diff --git a/user/test/com/google/gwt/user/UISuite.java b/user/test/com/google/gwt/user/UISuite.java
index 079dab7..507a5a4 100644
--- a/user/test/com/google/gwt/user/UISuite.java
+++ b/user/test/com/google/gwt/user/UISuite.java
@@ -39,6 +39,7 @@
 import com.google.gwt.user.client.ui.DecoratedTabBarTest;
 import com.google.gwt.user.client.ui.DecoratedTabPanelTest;
 import com.google.gwt.user.client.ui.DecoratorPanelTest;
+import com.google.gwt.user.client.ui.DefaultSuggestionDisplayTest;
 import com.google.gwt.user.client.ui.DelegatingKeyboardListenerCollectionTest;
 import com.google.gwt.user.client.ui.DialogBoxTest;
 import com.google.gwt.user.client.ui.DisclosurePanelTest;
@@ -122,6 +123,7 @@
     suite.addTestSuite(DecoratedTabBarTest.class);
     suite.addTestSuite(DecoratedTabPanelTest.class);
     suite.addTestSuite(DecoratorPanelTest.class);
+    suite.addTestSuite(DefaultSuggestionDisplayTest.class);
     suite.addTestSuite(DelegatingKeyboardListenerCollectionTest.class);
     suite.addTestSuite(DialogBoxTest.class);
     suite.addTestSuite(DisclosurePanelTest.class);
diff --git a/user/test/com/google/gwt/user/client/ui/DefaultSuggestionDisplayTest.java b/user/test/com/google/gwt/user/client/ui/DefaultSuggestionDisplayTest.java
new file mode 100644
index 0000000..44b8c7d
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/DefaultSuggestionDisplayTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010 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.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+
+import java.util.List;
+
+/**
+ * Tests for {@link DefaultSuggestionDisplay}.
+ */
+public class DefaultSuggestionDisplayTest extends SuggestionDisplayTestBase {
+
+  public void testAccessors() {
+    SuggestBox box = createSuggestBox();
+    DefaultSuggestionDisplay display = (DefaultSuggestionDisplay) box.getSuggestionDisplay();
+    PopupPanel popup = display.getPopupPanel();
+
+    // isAnimationEnabled.
+    assertFalse(display.isAnimationEnabled());
+    assertFalse(popup.isAnimationEnabled());
+    display.setAnimationEnabled(true);
+    assertTrue(display.isAnimationEnabled());
+    assertTrue(popup.isAnimationEnabled());
+
+    // isSuggestListShowing.
+    List<Suggestion> suggestions = createSuggestions("test0", "test1", "test2");
+    assertFalse(display.isSuggestionListShowing());
+    assertFalse(popup.isShowing());
+    display.showSuggestions(box, suggestions, false, false, NULL_CALLBACK);
+    assertTrue(display.isSuggestionListShowing());
+    assertTrue(popup.isShowing());
+    display.hideSuggestions();
+    assertFalse(display.isSuggestionListShowing());
+    assertFalse(popup.isShowing());
+  }
+
+  public void testGetCurrentSelectionWhenHidden() {
+    SuggestBox box = createSuggestBox();
+    DefaultSuggestionDisplay display = (DefaultSuggestionDisplay) box.getSuggestionDisplay();
+
+    // Show the suggestions and select the first item.
+    List<Suggestion> suggestions = createSuggestions("test0", "test1", "test2");
+    display.showSuggestions(box, suggestions, false, true, NULL_CALLBACK);
+    assertTrue(display.isSuggestionListShowing());
+    assertEquals(suggestions.get(0), display.getCurrentSelection());
+
+    // Hide the list and ensure that nothing is selected.
+    display.hideSuggestions();
+    assertNull(display.getCurrentSelection());
+  }
+
+  public void testShowSuggestionsEmpty() {
+    SuggestBox box = createSuggestBox();
+    DefaultSuggestionDisplay display = (DefaultSuggestionDisplay) box.getSuggestionDisplay();
+
+    // Show null suggestions.
+    display.showSuggestions(box, null, false, true, NULL_CALLBACK);
+    assertFalse(display.isSuggestionListShowing());
+
+    // Show empty suggestions.
+    List<Suggestion> suggestions = createSuggestions();
+    display.showSuggestions(box, suggestions, false, true, NULL_CALLBACK);
+    assertFalse(display.isSuggestionListShowing());
+  }
+
+  @Override
+  protected DefaultSuggestionDisplay createSuggestionDisplay() {
+    return new DefaultSuggestionDisplay();
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/SuggestBoxTest.java b/user/test/com/google/gwt/user/client/ui/SuggestBoxTest.java
index feb84c1..8bf2db5 100644
--- a/user/test/com/google/gwt/user/client/ui/SuggestBoxTest.java
+++ b/user/test/com/google/gwt/user/client/ui/SuggestBoxTest.java
@@ -17,14 +17,57 @@
 
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
-import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestBox.SuggestionCallback;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
 
 /**
  * Tests for {@link SuggestBoxTest}.
  */
-public class SuggestBoxTest extends GWTTestCase {
+public class SuggestBoxTest extends WidgetTestBase {
+
+  /**
+   * A SuggestionDisplay used for testing.
+   */
+  private static class TestSuggestionDisplay extends DefaultSuggestionDisplay {
+
+    private List<? extends Suggestion> suggestions;
+
+    @Override
+    protected void showSuggestions(SuggestBox suggestBox,
+        Collection<? extends Suggestion> suggestions,
+        boolean isDisplayStringHTML, boolean isAutoSelectEnabled,
+        SuggestionCallback callback) {
+      super.showSuggestions(suggestBox, suggestions, isDisplayStringHTML,
+          isAutoSelectEnabled, callback);
+      this.suggestions = new ArrayList<Suggestion>(suggestions);
+    }
+
+    /**
+     * Get the suggestion at the specified index.
+     * 
+     * @param index the index
+     * @return the {@link Suggestion} at the index
+     */
+    public Suggestion getSuggestion(int index) {
+      return suggestions.get(index);
+    }
+
+    /**
+     * Get the number of suggestions that are currently showing. Used for
+     * testing.
+     * 
+     * @return the number of suggestions currently showing, 0 if there are none
+     */
+    public int getSuggestionCount() {
+      return suggestions.size();
+    }
+  }
 
   @Override
   public String getModuleName() {
@@ -34,6 +77,7 @@
   /**
    * Test the basic accessors.
    */
+  @SuppressWarnings("deprecation")
   public void testAccessors() {
     SuggestBox box = createSuggestBox();
 
@@ -53,44 +97,48 @@
     assertTrue(box.isSuggestionListShowing());
   }
 
+  @SuppressWarnings("deprecation")
   public void testShowAndHide() {
     SuggestBox box = createSuggestBox();
-    assertFalse(box.isSuggestionListShowing());
+    TestSuggestionDisplay display = (TestSuggestionDisplay) box.getSuggestionDisplay();
+    assertFalse(display.isSuggestionListShowing());
+
     // should do nothing, box is not attached.
     box.showSuggestionList();
-    assertFalse(box.isSuggestionListShowing());
+    assertFalse(display.isSuggestionListShowing());
 
     // Adds the suggest box to the root panel.
     RootPanel.get().add(box);
-    assertFalse(box.isSuggestionListShowing());
+    assertFalse(display.isSuggestionListShowing());
 
     // Hides the list of suggestions, should be a no-op.
     box.hideSuggestionList();
 
     // Should try to show, but still fail, as there are no default suggestions.
     box.showSuggestionList();
-    assertFalse(box.isSuggestionListShowing());
+    assertFalse(display.isSuggestionListShowing());
 
     // Now, finally, should be true
     box.setText("t");
     box.showSuggestionList();
-    assertTrue(box.isSuggestionListShowing());
+    assertTrue(display.isSuggestionListShowing());
 
     // Hides it for real this time.
     box.hideSuggestionList();
-    assertFalse(box.isSuggestionListShowing());
+    assertFalse(display.isSuggestionListShowing());
   }
 
   public void testDefaults() {
     MultiWordSuggestOracle oracle = new MultiWordSuggestOracle();
     oracle.setDefaultSuggestionsFromText(Arrays.asList("A", "B"));
-    SuggestBox box = new SuggestBox(oracle);
+    TestSuggestionDisplay display = new TestSuggestionDisplay();
+    SuggestBox box = new SuggestBox(oracle, new TextBox(), display);
     RootPanel.get().add(box);
     box.showSuggestionList();
-    assertTrue(box.isSuggestionListShowing());
-    assertEquals(2, box.getSuggestionCount());
-    assertEquals("A", box.getSuggestion(0).getReplacementString());
-    assertEquals("B", box.getSuggestion(1).getReplacementString());
+    assertTrue(display.isSuggestionListShowing());
+    assertEquals(2, display.getSuggestionCount());
+    assertEquals("A", display.getSuggestion(0).getReplacementString());
+    assertEquals("B", display.getSuggestion(1).getReplacementString());
   }
 
   public void testShowFirst() {
@@ -106,12 +154,6 @@
     // text box and ensure that we see the correct behavior.
   }
 
-  @Override
-  public void gwtTearDown() throws Exception {
-    super.gwtTearDown();
-    RootPanel.get().clear();
-  }
-
   public void testWrapUsingStaticWrapMethod() {
     Element wrapper = Document.get().createTextInputElement();
     RootPanel.get().getElement().appendChild(wrapper);
@@ -133,7 +175,7 @@
 
   protected SuggestBox createSuggestBox() {
     MultiWordSuggestOracle oracle = createOracle();
-    return new SuggestBox(oracle);
+    return new SuggestBox(oracle, new TextBox(), new TestSuggestionDisplay());
   }
 
   private MultiWordSuggestOracle createOracle() {
diff --git a/user/test/com/google/gwt/user/client/ui/SuggestionDisplayTestBase.java b/user/test/com/google/gwt/user/client/ui/SuggestionDisplayTestBase.java
new file mode 100644
index 0000000..ad9188b
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/SuggestionDisplayTestBase.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2010 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.SuggestBox.SuggestionCallback;
+import com.google.gwt.user.client.ui.SuggestBox.SuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base tests for {@link SuggestionDisplay}.
+ */
+public abstract class SuggestionDisplayTestBase extends WidgetTestBase {
+
+  /**
+   * A no-op callback used for testing.
+   */
+  protected static final SuggestionCallback NULL_CALLBACK = new SuggestionCallback() {
+    public void onSuggestionSelected(Suggestion suggestion) {
+    }
+  };
+
+  /**
+   * A simple {@link Suggestion} implementation that uses a single string for
+   * both the display and replacement string.
+   */
+  private static class SimpleSuggestion implements Suggestion {
+
+    public String text;
+
+    public SimpleSuggestion(String text) {
+      this.text = text;
+    }
+
+    public String getDisplayString() {
+      return text;
+    }
+
+    public String getReplacementString() {
+      return text;
+    }
+  }
+
+  public void testMoveSelectionUpAndDown() {
+    SuggestBox box = new SuggestBox();
+    SuggestionDisplay display = box.getSuggestionDisplay();
+    SuggestOracle oracle = box.getSuggestOracle();
+
+    // Show some suggestions.
+    List<Suggestion> suggestions = createSuggestions("test0", "test1", "test2",
+        "test3");
+    display.showSuggestions(box, suggestions, false, false, NULL_CALLBACK);
+    assertNull(display.getCurrentSelection());
+
+    display.moveSelectionDown();
+    assertEquals(suggestions.get(0), display.getCurrentSelection());
+    display.moveSelectionDown();
+    assertEquals(suggestions.get(1), display.getCurrentSelection());
+    display.moveSelectionDown();
+    assertEquals(suggestions.get(2), display.getCurrentSelection());
+    display.moveSelectionUp();
+    assertEquals(suggestions.get(1), display.getCurrentSelection());
+    display.moveSelectionUp();
+    assertEquals(suggestions.get(0), display.getCurrentSelection());
+  }
+
+  public void testShowSuggestionsAutoSelectDisabled() {
+    SuggestBox box = new SuggestBox();
+    SuggestionDisplay display = box.getSuggestionDisplay();
+    SuggestOracle oracle = box.getSuggestOracle();
+
+    // Show some suggestions with auto select disabled.
+    List<Suggestion> suggestions = createSuggestions("test0", "test1", "test2");
+    display.showSuggestions(box, suggestions, false, false, NULL_CALLBACK);
+
+    // Nothing should be selected.
+    assertNull(display.getCurrentSelection());
+  }
+
+  public void testShowSuggestionsAutoSelectEnabled() {
+    SuggestBox box = new SuggestBox();
+    SuggestionDisplay display = box.getSuggestionDisplay();
+    SuggestOracle oracle = box.getSuggestOracle();
+
+    // Show some suggestions with auto select enabled.
+    List<Suggestion> suggestions = createSuggestions("test0", "test1", "test2");
+    display.showSuggestions(box, suggestions, false, true, NULL_CALLBACK);
+
+    // First item should be selected.
+    assertEquals(suggestions.get(0), display.getCurrentSelection());
+  }
+
+  /**
+   * Create a list of {@link Suggestion}.
+   * 
+   * @param items the items to add to the list
+   * @return the list of suggestions
+   */
+  protected List<Suggestion> createSuggestions(String... items) {
+    List<Suggestion> suggestions = new ArrayList<Suggestion>();
+    for (String item : items) {
+      suggestions.add(new SimpleSuggestion(item));
+    }
+    return suggestions;
+  }
+
+  /**
+   * Create a new {@link SuggestionDisplay} to test.
+   * 
+   * @return the {@link SuggestionDisplay}
+   */
+  protected abstract SuggestionDisplay createSuggestionDisplay();
+
+  /**
+   * Create a new {@link SuggestBox}.
+   * 
+   * @return the {@link SuggestBox}
+   */
+  protected SuggestBox createSuggestBox() {
+    MultiWordSuggestOracle oracle = createOracle();
+    return new SuggestBox(oracle, new TextBox(), createSuggestionDisplay());
+  }
+
+  private MultiWordSuggestOracle createOracle() {
+    MultiWordSuggestOracle oracle = new MultiWordSuggestOracle();
+    oracle.add("test");
+    oracle.add("test1");
+    oracle.add("test2");
+    oracle.add("test3");
+    oracle.add("test4");
+    oracle.add("john");
+    return oracle;
+  }
+}