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_TAB; import static java.awt.event.KeyEvent.VK_UP; import java.awt.Font; import java.awt.Rectangle; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.HierarchyBoundsAdapter; import java.awt.event.HierarchyEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; 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.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import org.geogebra.desktop.gui.inputfield.AutoCompleteTextFieldD; /** * Provides completion popup for {@link AutoCompleteTextFieldD}. Derived from * OptionsPopup. * * @author Arnaud Delobelle */ public class CompletionsPopup { private final AutoCompleteTextFieldD textField; private final int maxPopupRowCount; private final JPopupMenu popup; private final DelegatingListModel listModel; private final JList list; private DocumentListener textFieldDocListener; private KeyListener keyListener; private KeyListener[] textFieldKeyListeners; private int current_length; /** * Initializes components and registers event listeners. * * @param textField * The text field * @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 CompletionsPopup(AutoCompleteTextFieldD textField, ListCellRenderer listCellRenderer, int maxPopupRowCount) { this.textField = textField; 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); current_length = -1;// current length of sentence registerListeners(); } /** * Set the font to display the completions * * @param font * the new font */ public void setFont(Font font) { list.setFont(font); } private class PopupListener implements PopupMenuListener { @Override public void popupMenuCanceled(PopupMenuEvent e) { // ignore } @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { textField.removeKeyListener(keyListener); for (KeyListener listener : textFieldKeyListeners) { textField.addKeyListener(listener); } } @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { // Remove key listeners and replace with own; textFieldKeyListeners = textField.getKeyListeners(); for (KeyListener listener : textFieldKeyListeners) { textField.removeKeyListener(listener); } textField.addKeyListener(keyListener); } } private void registerListeners() { // Suggest completions on text changes, store reference to listener // object textFieldDocListener = new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { // only handle insert } @Override public void insertUpdate(DocumentEvent e) { /* showCompletions(); */ if (current_length != e.getOffset()) { hidePopup(); current_length = e.getOffset(); } } @Override public void changedUpdate(DocumentEvent e) { // only handle insert } }; textField.getDocument().addDocumentListener(textFieldDocListener); // Handle special keys (e.g. navigation) keyListener = 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) { hidePopup(); current_length = -1; } }); // Allow the user click on an option for completion list.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { handleMouseClick(e); } }); // This doesn't work very well :( textField.addHierarchyBoundsListener(new HierarchyBoundsAdapter() { @Override public void ancestorMoved(HierarchyEvent e) { if (isPopupVisible()) { placePopup(); } } }); popup.addPopupMenuListener(new PopupListener()); } protected void placePopup() { Rectangle startRect; try { startRect = textField.modelToView(textField.getCurrentWordStart()); } catch (BadLocationException e) { // This won't happen of course :) startRect = new Rectangle(0, 0, 0, 0); } // Try to show popup just beneath the word to be completed popup.show(textField, startRect.x, startRect.y + startRect.height); // If it overlaps the word, then show the popup above the word if (popup.getLocationOnScreen().y - textField.getLocationOnScreen().y < startRect.y + startRect.height) { popup.show(textField, startRect.x, startRect.y - popup.getHeight()); } } public void showCompletions() { if (!textField.getAutoComplete()) { return; } List<String> completions = textField.getCompletions(); if (completions == null) { hidePopup(); return; } if (completions.size() > 0) { listModel.setDataList(completions); list.setSelectedIndex(0); list.ensureIndexIsVisible(0); showPopup(); } else { hidePopup(); } } private void showPopup() { // Adjust size of the popup if necessary int newPopupRowCount = Math.min(listModel.getSize(), maxPopupRowCount); list.setVisibleRowCount(newPopupRowCount); // Let the UI calculate the preferred size popup.setPreferredSize(null); popup.pack(); placePopup(); } private boolean isPopupVisible() { return popup.isVisible(); } private void hidePopup() { if (!isPopupVisible()) { return; } popup.setVisible(false); list.clearSelection(); // Reinstate textField's key listeners } public void handleSpecialKeys(KeyEvent keyEvent) { if (!isPopupVisible()) { return; } switch (keyEvent.getKeyCode()) { case VK_ESCAPE: // [ESC] cancels the popup textField.cancelAutoCompletion(); hidePopup(); keyEvent.consume(); break; case VK_ENTER: // [ENTER] validates the completions textField.validateAutoCompletion(list.getSelectedIndex(), textField.getCompletions()); hidePopup(); keyEvent.consume(); break; case VK_DOWN: // [DOWN] next completion case VK_TAB: // [TAB] navigateRelative(+1); keyEvent.consume(); break; case VK_UP: // [UP] prev. completion navigateRelative(-1); keyEvent.consume(); break; case VK_PAGE_DOWN: // [PAGE_DOWN] navigateRelative(+maxPopupRowCount - 1); keyEvent.consume(); break; case VK_PAGE_UP: // [PAGE_UP] navigateRelative(-maxPopupRowCount + 1); keyEvent.consume(); break; default: hidePopup(); current_length = -1; textField.processKeyEvent(keyEvent); } } 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); } } private void handleMouseClick(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { textField.validateAutoCompletion(list.getSelectedIndex(), textField.getCompletions()); hidePopup(); current_length = -1; } } }