/***************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.cayenne.modeler.util.combo; import javax.swing.JComboBox; import javax.swing.JList; import javax.swing.SwingUtilities; import javax.swing.text.JTextComponent; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; /** * AutoCompletion class handles user input and suggests matching variants (see CAY-911) * */ public class AutoCompletion implements FocusListener, KeyListener, Runnable { /** * Property to mark combobox as 'auto-completing' */ public static final String AUTOCOMPLETION_PROPERTY = "JComboBox.autoCompletion"; /** * A list with matching items */ private final SuggestionList suggestionList; /** * Combo with auto-completion */ private final JComboBox comboBox; private final JTextComponent textEditor; private final boolean allowsUserValues; protected AutoCompletion(final JComboBox comboBox, boolean strict, boolean allowsUserValues) { this.comboBox = comboBox; textEditor = ((JTextComponent)comboBox.getEditor().getEditorComponent()); this.allowsUserValues = allowsUserValues; suggestionList = new SuggestionList(comboBox, strict); // Marking combobox as auto-completing comboBox.putClientProperty(AUTOCOMPLETION_PROPERTY, Boolean.TRUE); } /** * Enables auto-completion for specified combobox * * @param comboBox Combo to be featured * @param strict Whether strict matching (check 'startWith' or 'contains') should be used * @param allowsUserValues Whether non-present items are allowed */ public static void enable(JComboBox comboBox, boolean strict, boolean allowsUserValues) { comboBox.setEditable(true); KeyListener[] listeners = comboBox.getEditor().getEditorComponent().getListeners(KeyListener.class); comboBox.setEditor(new CustomTypeComboBoxEditor(comboBox, allowsUserValues)); for (KeyListener listener : listeners) { comboBox.getEditor().getEditorComponent().addKeyListener(listener); } AutoCompletion ac = new AutoCompletion(comboBox, strict, allowsUserValues); comboBox.addFocusListener(ac); ac.textEditor.addKeyListener(ac); //original keys would not work properly SwingUtilities.replaceUIActionMap(comboBox, null); } /** * Enables auto-completion for specified combobox */ public static void enable(JComboBox comboBox) { enable(comboBox, true, false); } public void focusGained(FocusEvent e) {} public void focusLost(FocusEvent e) { suggestionList.hide(); } public void keyPressed(KeyEvent e) { handleKeyPressed(e); } public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE || e.getKeyCode() == KeyEvent.VK_ENTER) { String text = textEditor.getText(); if (comboBox.isShowing()) { suggestionList.hide(); suggestionList.filter(text); suggestionList.show(); } } } public void keyTyped(KeyEvent e) {} public void run() { String text = textEditor.getText(); //need to hide first because Swing incorrectly updates popups (getSize() returns //dimension not the same as seen on the screen) suggestionList.hide(); if (comboBox.isShowing()) { suggestionList.filter(text); if (suggestionList.getItemCount() > 0) { suggestionList.show(); } } } /** * Calculates next selection row, according to a pressed key and selects it. * This might affect either suggestion list or original popup */ private void handleKeyPressed(KeyEvent e) { boolean suggest = suggestionList.isVisible(); if (suggest) { processKeyPressedWhenSuggestionListIsVisible(e); } else { processKeyPressedWhenSuggestionListIsInvisible(e); } //scroll doesn't work in suggestionList..so we will scroll manually suggestionListScrolling(); } private void processKeyPressedWhenSuggestionListIsInvisible(KeyEvent e){ int sel = comboBox.getSelectedIndex(); int max = comboBox.getItemCount() - 1; int next; switch (e.getKeyCode()) { case KeyEvent.VK_UP: case KeyEvent.VK_NUMPAD8: next = sel - 1; break; case KeyEvent.VK_DOWN: case KeyEvent.VK_NUMPAD2: next = sel + 1; break; case KeyEvent.VK_PAGE_UP: next = sel - comboBox.getMaximumRowCount(); break; case KeyEvent.VK_PAGE_DOWN: next = sel + comboBox.getMaximumRowCount(); break; case KeyEvent.VK_HOME: next = 0; break; case KeyEvent.VK_END: next = max; break; case KeyEvent.VK_ENTER: case KeyEvent.VK_ESCAPE: return; default: //invoke in end of AWT thread so that information in textEditor would update SwingUtilities.invokeLater(this); return; } e.consume(); handleNavigationKeys(false,next,sel,max); } private void processKeyPressedWhenSuggestionListIsVisible(KeyEvent e){ int sel = suggestionList.getSelectedIndex(); int max = suggestionList.getItemCount() - 1; int next; switch (e.getKeyCode()) { case KeyEvent.VK_UP: case KeyEvent.VK_NUMPAD8: next = sel - 1; break; case KeyEvent.VK_DOWN: case KeyEvent.VK_NUMPAD2: next = sel + 1; break; case KeyEvent.VK_PAGE_UP: next = sel - comboBox.getMaximumRowCount(); break; case KeyEvent.VK_PAGE_DOWN: next = sel + comboBox.getMaximumRowCount(); break; case KeyEvent.VK_HOME: next = 0; break; case KeyEvent.VK_END: next = max; break; case KeyEvent.VK_ENTER: processEnterPressed(); return; case KeyEvent.VK_ESCAPE: suggestionList.hide(); return; default: //invoke in end of AWT thread so that information in textEditor would update SwingUtilities.invokeLater(this); return; } e.consume(); handleNavigationKeys(true,next,sel,max); } private void processEnterPressed(){ Object value = suggestionList.getSelectedValue(); if (!allowsUserValues && value == null && suggestionList.getItemCount() > 0) { value = suggestionList.getItemAt(0); } //reset the item (value == null) only if user values are not supported if (value != null || !allowsUserValues) { comboBox.setSelectedItem(value); } suggestionList.hide(); } private void handleNavigationKeys(boolean suggest, int next, int sel, int max){ if (!suggest && !comboBox.isPopupVisible()) { comboBox.setPopupVisible(true); return; } if (comboBox.getItemCount() > 0) { if (next < 0) { next = 0; } if (next > max) { next = max; } if (next != sel) { if (suggest) { suggestionList.setSelectedIndex(next); } else { comboBox.setPopupVisible(true); comboBox.setSelectedIndex(next); } } textEditor.requestFocus(); } } private void suggestionListScrolling(){ JList list = suggestionList.getList(); int selectedIndex = suggestionList.getSelectedIndex(); list.ensureIndexIsVisible(selectedIndex); } }