package org.geogebra.desktop.gui.autocompletion; import static java.awt.event.KeyEvent.VK_DOWN; import static java.awt.event.KeyEvent.VK_ENTER; import static java.awt.event.KeyEvent.VK_ESCAPE; import static java.awt.event.KeyEvent.VK_PAGE_DOWN; import static java.awt.event.KeyEvent.VK_PAGE_UP; import static java.awt.event.KeyEvent.VK_UP; import java.awt.Dimension; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.List; import javax.swing.BorderFactory; import javax.swing.JList; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.Document; /** * Provides infrastructure for realizing a simple auto completion mechanism for * {@link JTextField} components. A user should prefer one of the static * <code>install</code> methods in the {@link AutoCompletion} class over * instantiating this class directly. * * @param <T> * The type of the displayed completion options * * @author Julian Lettner */ public class OptionsPopup<T> { private final JTextField textField; private final CompletionProvider<T> completionProvider; private final int maxPopupRowCount; private final JPopupMenu popup; private final DelegatingListModel listModel; private final JList list; private DocumentListener documentListener; private String userInput; private int popupRowCount; /** * Initializes components and registers event listeners. * * @param textField * The text field * @param completionProvider * A completion provider (The returned values will be the input * for the supplied {@link ListCellRenderer}) * @param listCellRenderer * A list cell renderer which visualizes the options returned by * the provided {@link CompletionProvider} * @param maxPopupRowCount * The maximal number of rows for the options popup */ public OptionsPopup(JTextField textField, CompletionProvider<T> completionProvider, ListCellRenderer listCellRenderer, int maxPopupRowCount) { this.textField = textField; this.completionProvider = completionProvider; this.maxPopupRowCount = maxPopupRowCount; // Initialize components listModel = new DelegatingListModel(); list = new JList(listModel); list.setCellRenderer(listCellRenderer); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setFocusable(false); popup = new JPopupMenu(); popup.add(new JScrollPane(list)); popup.setBorder(BorderFactory.createEmptyBorder()); popup.setFocusable(false); registerListeners(); } private void registerListeners() { // Suggest completions on text changes, store reference to listener // object documentListener = new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { showCompletion(); } @Override public void insertUpdate(DocumentEvent e) { showCompletion(); } @Override public void changedUpdate(DocumentEvent e) { showCompletion(); } }; textField.getDocument().addDocumentListener(documentListener); // Handle special keys (e.g. navigation) textField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { handleSpecialKeys(e); } }); // Hide popup when text field loses focus textField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { hideOptionsPopup(); } }); // Allow the user click on a option for completion list.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { handleMouseClick(e); } }); } private void showCompletion() { userInput = textField.getText(); if (null == userInput || 0 == userInput.length()) { hideOptionsPopup(); return; } List<?> options = completionProvider.getCompletionOptions(userInput); if (null != options && 0 != options.size()) { listModel.setDataList(options); showOptionsPopup(); } else { hideOptionsPopup(); } } private void showOptionsPopup() { // Adjust size of the popup if necessary int newPopupRowCount = Math.min(listModel.getSize(), maxPopupRowCount); adjustPopupSize(newPopupRowCount); // Show popup just beneath the text field if (!isOptionsPopupVisible()) { popup.show(textField, 0, textField.getHeight()); } } // Adjusts the size of the popup (tricky) private void adjustPopupSize(int newPopupRowCount) { if (popupRowCount == newPopupRowCount) { return; } popupRowCount = newPopupRowCount; // Set visible row count in list list.setVisibleRowCount(popupRowCount); // Let the UI calculate the preferred size popup.setPreferredSize(null); // Get the preferred size from the UI Dimension size = popup.getPreferredSize(); // Overwrite width size.width = textField.getWidth(); // Set the preferred size popup.setPreferredSize(size); popup.pack(); } private boolean isOptionsPopupVisible() { return popup.isVisible(); } private void hideOptionsPopup() { if (isOptionsPopupVisible()) { popup.setVisible(false); list.clearSelection(); } } private void updateText() { T option = (T) list.getSelectedValue(); String text = option == null ? userInput : completionProvider.toString(option); Document d = textField.getDocument(); d.removeDocumentListener(documentListener); textField.setText(text); d.addDocumentListener(documentListener); } private void handleSpecialKeys(KeyEvent keyEvent) { if (!isOptionsPopupVisible()) { return; } switch (keyEvent.getKeyCode()) { default: // do nothing break; case VK_ESCAPE: // [ESC] hideOptionsPopup(); keyEvent.consume(); break; case VK_ENTER: // [ENTER] hideOptionsPopup(); textField.selectAll(); break; case VK_DOWN: // [DOWN] navigateRelative(+1); break; case VK_UP: // [UP] navigateRelative(-1); break; case VK_PAGE_DOWN: // [PAGE_DOWN] navigateRelative(+maxPopupRowCount - 1); break; case VK_PAGE_UP: // [PAGE_UP] navigateRelative(-maxPopupRowCount + 1); break; } } private void navigateRelative(int offset) { boolean up = offset < 0; int end = listModel.getSize() - 1; int index = list.getSelectedIndex(); // Wrap around if (-1 == index) { index = up ? end : 0; } else if (0 == index && up || end == index && !up) { index = -1; } else { index += offset; index = Math.max(0, Math.min(end, index)); } if (-1 == index) { list.clearSelection(); } else { list.setSelectedIndex(index); list.ensureIndexIsVisible(index); } updateText(); } private void handleMouseClick(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { updateText(); hideOptionsPopup(); textField.selectAll(); } } }