package org.limewire.ui.swing.components; import java.awt.Component; import java.awt.Dimension; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.BorderFactory; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import org.limewire.concurrent.FutureEvent; import org.limewire.concurrent.ListeningFuture; import org.limewire.listener.EventListener; import org.limewire.listener.SwingEDTEvent; import org.limewire.ui.swing.components.AutoCompleter.AutoCompleterCallback; /** A DropDown list of autocompletable items for a JTextField. */ public class DropDownListAutoCompleteControl { private static final String PROPERTY = "limewire.text.autocompleteControl"; /** The text field this is working on. */ private final JTextField textField; /** The autocompleter. */ private final AutoCompleter autoCompleter; /** The popup the scroll pane is in. */ protected JPopupMenu popup; /** Whether or not we tried to show a popup while this wasn't showing */ protected boolean showPending; /** Whether or not this control should try to autocomplete input. */ private boolean autoComplete = true; /** Installs a dropdown list for autocompletion on the given text field. */ public static DropDownListAutoCompleteControl install(final JTextField textField) { final BasicAutoCompleter basicAutoCompleter = new BasicAutoCompleter(); textField.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { basicAutoCompleter.addAutoCompleteSuggestion(textField.getText()); } }); return install(textField, basicAutoCompleter); } /** Installs a dropdown list using the given autocompleter. */ public static DropDownListAutoCompleteControl install(JTextField textField, AutoCompleter autoCompleter) { DropDownListAutoCompleteControl control = new DropDownListAutoCompleteControl(textField, autoCompleter); textField.putClientProperty(PROPERTY, control); Listener listener = control.new Listener(); textField.addKeyListener(listener); textField.addHierarchyListener(listener); textField.addFocusListener(listener); textField.addActionListener(listener); autoCompleter.setAutoCompleterCallback(listener); return control; } /** Returns the control for the text field. */ public static DropDownListAutoCompleteControl getDropDownListAutoCompleteControl(JTextField textField) { return (DropDownListAutoCompleteControl)textField.getClientProperty(PROPERTY); } private DropDownListAutoCompleteControl(JTextField textField, AutoCompleter autoCompleter) { this.textField = textField; this.autoCompleter = autoCompleter; } /** * Sets whether the component is currently performing autocomplete lookups as * keystrokes are performed. * * @param autoComplete true or false. */ public void setAutoComplete(boolean autoComplete) { this.autoComplete = autoComplete; } /** * Gets whether the component is currently performing autocomplete lookups as * keystrokes are performed. * * @return true or false. */ public boolean isAutoCompleting() { return autoComplete; } /** * Displays the popup window with a list of auto-completable choices, * if any exist. */ private void autoCompleteInput() { // Shove this into an invokeLater to force us seeing the proper text. if(isAutoCompleting()) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { String input = textField.getText(); if (input != null && input.length() > 0) { ListeningFuture<Boolean> future = autoCompleter.setInput(input); // If it finished immediately, don't hide it! if(!future.isDone()) { hidePopup(); } future.addFutureListener(new EventListener<FutureEvent<Boolean>>() { @Override @SwingEDTEvent public void handleEvent(FutureEvent<Boolean> event) { // == Boolean.TRUE to prevent NPE on unbox if result is null if(event.getResult() == Boolean.TRUE) { showPopup(); } else { hidePopup(); } } }); } else { hidePopup(); } } }); } } /** * Creates a new popup containing the specified component. */ private JPopupMenu createPopup(Component component) { // Create popup. We only display the border for the popup component, // so we remove it from the popup container. Also, we want to keep the // focus within the text field, so the popup should not be focusable. JPopupMenu popupMenu = new JPopupMenu(); popupMenu.setBorder(BorderFactory.createEmptyBorder()); popupMenu.setFocusable(false); popupMenu.add(component); // Add listener to reset popup reference when hidden. popupMenu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuCanceled(PopupMenuEvent e) { } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { resetPopup(); } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } }); return popupMenu; } /** Shows the popup. */ private void showPopup() { if(autoCompleter.isAutoCompleteAvailable()) { if(textField.isShowing()) { Component parent = textField; JComponent component = autoCompleter.getRenderComponent(); // Adjust popup position for text field with painted border. int leftInset = 0; int bottomInset = 0; int widthInset = 0; if (textField instanceof Paintable) { Insets paintedInsets = ((Paintable) textField).getPaintedInsets(); leftInset = paintedInsets.left; bottomInset = paintedInsets.bottom; widthInset = paintedInsets.left + paintedInsets.right; } // Null out our prior preferred size, then set a new one // that overrides the width to be the size we want it, but // preserves the height. Dimension priorPref = component.getPreferredSize(); component.setPreferredSize(null); Dimension pref = component.getPreferredSize(); pref = new Dimension(textField.getWidth() - widthInset, pref.height+10); component.setPreferredSize(pref); if(popup != null && priorPref.equals(pref)) { return; // no need to change if sizes are same. } // If the popup exists already, hide it & reshow it to make it the right size. if (popup != null) { hidePopup(); } popup = createPopup(component); showPending = false; popup.show(parent, leftInset, textField.getHeight() - bottomInset); } else { showPending = true; } } } /** Hides the popup window. */ private void hidePopup() { if (popup != null) { popup.setVisible(false); } resetPopup(); } /** Resets popup references. Called when popup is hidden. */ private void resetPopup() { showPending = false; popup = null; } private class Listener implements ActionListener, KeyListener, HierarchyListener, FocusListener, AutoCompleterCallback { @Override public void itemSuggested(String autoCompleteString, boolean keepPopupVisible, boolean triggerAction) { textField.setText(autoCompleteString); textField.setCaretPosition(textField.getDocument().getLength()); if(triggerAction) { textField.postActionEvent(); } else if(!keepPopupVisible) { hidePopup(); } } /** * Fires an action event. * <p> * If the popup is visible, this resets the current * text to be the selection on the popup (if something was selected) * prior to firing the event. */ @Override public void actionPerformed(ActionEvent e) { if(popup != null) { String selection = autoCompleter.getSelectedAutoCompleteString(); hidePopup(); if(selection != null) { textField.setText(selection); } } } /** Forwards necessary events to the AutoCompleteList. */ @Override public void keyPressed(KeyEvent evt) { if(evt.getKeyCode() == KeyEvent.VK_UP || evt.getKeyCode() == KeyEvent.VK_DOWN) { evt.consume(); } if(autoCompleter != null) { switch(evt.getKeyCode()) { case KeyEvent.VK_UP: if(popup != null) { autoCompleter.decrementSelection(); } else { String input = textField.getText(); autoCompleter.setInput(input).addFutureListener(new EventListener<FutureEvent<Boolean>>() { @Override @SwingEDTEvent public void handleEvent(FutureEvent<Boolean> event) { // == Boolean.TRUE to prevent NPE on unbox if result is null if(event.getResult() == Boolean.TRUE) { showPopup(); } } }); } break; case KeyEvent.VK_DOWN: if(popup != null) { autoCompleter.incrementSelection(); } else { String input = textField.getText(); autoCompleter.setInput(input).addFutureListener(new EventListener<FutureEvent<Boolean>>() { @Override @SwingEDTEvent public void handleEvent(FutureEvent<Boolean> event) { // == Boolean.TRUE to prevent NPE on unbox if result is null if(event.getResult() == Boolean.TRUE) { showPopup(); } } }); } break; case KeyEvent.VK_LEFT: case KeyEvent.VK_RIGHT: if(popup != null) { String selection = autoCompleter.getSelectedAutoCompleteString(); if(selection != null) { hidePopup(); } } break; } } } /** Forwards necessary events to the AutoCompleteList. */ @Override public void keyReleased(KeyEvent evt) { if(evt.getKeyCode() == KeyEvent.VK_UP || evt.getKeyCode() == KeyEvent.VK_DOWN) evt.consume(); } /** Forwards necessary events to the AutoCompleteList. */ @Override public void keyTyped(KeyEvent evt) { if(evt.getKeyCode() == KeyEvent.VK_UP || evt.getKeyCode() == KeyEvent.VK_DOWN) { evt.consume(); } if(autoCompleter != null) { switch(evt.getKeyChar()) { case KeyEvent.VK_ESCAPE: if (popup != null) { hidePopup(); textField.selectAll(); } break; case KeyEvent.VK_ENTER: break; default: autoCompleteInput(); } } } @Override public void hierarchyChanged(HierarchyEvent evt) { if((evt.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) == HierarchyEvent.SHOWING_CHANGED) { boolean showing = textField.isShowing(); if(!showing && popup != null) { hidePopup(); } else if(showing && popup == null && showPending) { autoCompleteInput(); } } } @Override public void focusGained(FocusEvent e) { } @Override public void focusLost(FocusEvent evt) { if (evt.getID() == FocusEvent.FOCUS_LOST && popup != null) { hidePopup(); } } } }