package org.chartsy.main.utils.autocomplete; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JComponent; import javax.swing.JList; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.Timer; 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 javax.swing.text.JTextComponent; import org.chartsy.main.SymbolChanger; /** * * @author Viorel */ public abstract class AutoCompleter { protected static final Logger LOG = Logger.getLogger(AutoCompleter.class.getPackage().getName()); protected Timer delayTimer; protected boolean timerStoped = false; protected JList list = new JList(); protected JPopupMenu popupMenu = new JPopupMenu(); protected JTextComponent component; private static final String AUTOCOMPLETER = "AUTOCOMPLETER"; public AutoCompleter(JTextComponent comp) { delayTimer = new Timer(500, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { timerStoped = false; try { showPopup(); } catch (IOException ex) { return; } } }); delayTimer.setRepeats(false); component = comp; component.putClientProperty(AUTOCOMPLETER, this); JScrollPane pane = new JScrollPane(list); pane.setBorder(null); list.setFocusable(true); pane.getVerticalScrollBar().setFocusable(false); pane.getHorizontalScrollBar().setFocusable(false); popupMenu.add(pane); if (component instanceof JTextField) { component.registerKeyboardAction( showAction, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), JComponent.WHEN_FOCUSED); component.getDocument().addDocumentListener(documentListener); } else { component.registerKeyboardAction( showAction, KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_MASK), JComponent.WHEN_FOCUSED); } component.registerKeyboardAction( upAction, KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), JComponent.WHEN_FOCUSED); component.registerKeyboardAction( hidePopupAction, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED); popupMenu.addPopupMenuListener(new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { component.unregisterKeyboardAction( KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)); if (component.getParent() instanceof SymbolChanger) { SymbolChanger changer = (SymbolChanger) component.getParent(); component.registerKeyboardAction( changer.submit, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED); } } @Override public void popupMenuCanceled(PopupMenuEvent e) {} }); list.setRequestFocusEnabled(false); } public void stopTimer() { delayTimer.stop(); timerStoped = true; } static Action acceptAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { JComponent comp = (JComponent) e.getSource(); AutoCompleter completer = (AutoCompleter) comp.getClientProperty(AUTOCOMPLETER); completer.popupMenu.setVisible(false); completer.stopTimer(); completer.acceptedListItem(completer.list.getSelectedValue()); } }; DocumentListener documentListener = new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { if (!timerStoped) { if (delayTimer.isRunning()) { timerStoped = false; delayTimer.restart(); } else { timerStoped = false; delayTimer.start(); } } } @Override public void removeUpdate(DocumentEvent e) { if (!timerStoped) { if (delayTimer.isRunning()) { timerStoped = false; delayTimer.restart(); } else { timerStoped = false; delayTimer.start(); } } } @Override public void changedUpdate(DocumentEvent e) {} }; private void showPopup() throws IOException { popupMenu.setVisible(false); if(component.isEnabled() && updateListData() && list.getModel().getSize()!=0) { if(!(component instanceof JTextField)) component.getDocument().addDocumentListener(documentListener); component.registerKeyboardAction( acceptAction, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED); int size = list.getModel().getSize(); list.setVisibleRowCount(size<10 ? size : 10); int x = 0; try { int pos = Math.min( component.getCaret().getDot(), component.getCaret().getMark()); x = component.getUI().modelToView(component, pos).x; } catch(BadLocationException e) { LOG.log(Level.SEVERE, null, e); } popupMenu.show(component, x, component.getHeight()); } else popupMenu.setVisible(false); component.requestFocus(); } static Action showAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { JComponent tf = (JComponent)e.getSource(); AutoCompleter completer = (AutoCompleter)tf.getClientProperty(AUTOCOMPLETER); if (tf.isEnabled()) { if(completer.popupMenu.isVisible()) completer.selectNextPossibleValue(); else { try { completer.showPopup(); } catch (IOException ex) { return; } } } } }; static Action upAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { JComponent tf = (JComponent)e.getSource(); AutoCompleter completer = (AutoCompleter)tf.getClientProperty(AUTOCOMPLETER); if (tf.isEnabled()) { if(completer.popupMenu.isVisible()) completer.selectPreviousPossibleValue(); } } }; static Action hidePopupAction = new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { JComponent tf = (JComponent)e.getSource(); AutoCompleter completer = (AutoCompleter)tf.getClientProperty(AUTOCOMPLETER); if (tf.isEnabled()) completer.popupMenu.setVisible(false); } }; /** * Selects the next item in the list. It won't change the selection if the * currently selected item is already the last item. */ protected void selectNextPossibleValue() { int si = list.getSelectedIndex(); if (si < list.getModel().getSize() - 1) { list.setSelectedIndex(si + 1); list.ensureIndexIsVisible(si + 1); } } /** * Selects the previous item in the list. It won't change the selection if the * currently selected item is already the first item. */ protected void selectPreviousPossibleValue() { int si = list.getSelectedIndex(); if(si > 0) { list.setSelectedIndex(si - 1); list.ensureIndexIsVisible(si - 1); } } // update list model depending on the data in textfield protected abstract boolean updateListData() throws IOException; // user has selected some item in the list. update textfield accordingly... protected abstract void acceptedListItem(Object selected); }