blob: 6631f46b272fc3188519f40baa4e36216c62e77a [file] [log] [blame]
/*
* 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.netbeans.modules.maven;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.DefaultListModel;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.openide.util.NbBundle;
/**
* inner class does the matching of the JTextField's
* document to completion strings kept in an ArrayList
* @author mkleint
*/
public class TextValueCompleter implements DocumentListener {
private static final String ACTION_FILLIN = "fill-in"; //NOI18N
private static final String ACTION_HIDEPOPUP = "hidepopup"; //NOI18N
private static final String ACTION_LISTDOWN = "listdown"; //NOI18N
private static final String ACTION_LISTPAGEDOWN = "listpagedown"; //NOI18N
private static final String ACTION_LISTUP = "listup"; //NOI18N
private static final String ACTION_LISTPAGEUP = "listpageup"; //NOI18N
private static final String ACTION_SHOWPOPUP = "showpopup"; //NOI18N
private Pattern pattern;
private Collection<String> completions;
private JList completionList;
private DefaultListModel<String> completionListModel;
private JScrollPane listScroller;
private Popup popup;
private JTextField field;
private String separators;
private CaretListener caretListener;
private boolean loading;
private static final String LOADING = Bundle.LBL_Loading();
private boolean partial;
public TextValueCompleter(Collection<String> completions, JTextField fld) {
this.completions = completions;
this.field = fld;
field.getDocument().addDocumentListener(this);
field.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
hidePopup();
}
});
caretListener = new CaretListener() {
@Override
public void caretUpdate(CaretEvent arg0) {
// only consider caret updates if the popup window is visible
if (completionList.isDisplayable() && completionList.isVisible()) {
buildAndShowPopup();
}
}
};
field.addCaretListener(caretListener);
completionListModel = new DefaultListModel<>();
completionList = new JList(completionListModel);
completionList.setFocusable(false);
completionList.setPrototypeCellValue("lets have it at least this wide and add some more just in case"); //NOI18N
completionList.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() > 1) {
if (LOADING.endsWith(completionList.getSelectedValue().toString())) {
return;
}
field.getDocument().removeDocumentListener(TextValueCompleter.this);
applyCompletion(completionList.getSelectedValue().toString());
hidePopup();
field.getDocument().addDocumentListener(TextValueCompleter.this);
}
}
});
completionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
listScroller =new JScrollPane(completionList,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
listScroller.setFocusable(false);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),ACTION_LISTDOWN);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),ACTION_LISTUP);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),ACTION_LISTPAGEUP);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),ACTION_LISTPAGEDOWN);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, KeyEvent.CTRL_DOWN_MASK),ACTION_SHOWPOPUP);
field.getActionMap().put(ACTION_LISTDOWN, new AbstractAction() { //NOI18N
@Override
public void actionPerformed(ActionEvent e) {
if (popup == null) {
buildAndShowPopup(0);
}
completionList.setSelectedIndex(Math.min(completionList.getSelectedIndex() + 1, completionList.getModel().getSize()));
completionList.ensureIndexIsVisible(completionList.getSelectedIndex());
}
});
field.getActionMap().put(ACTION_LISTUP, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
if (popup == null) {
buildAndShowPopup(0);
}
completionList.setSelectedIndex(Math.max(completionList.getSelectedIndex() - 1, 0));
completionList.ensureIndexIsVisible(completionList.getSelectedIndex());
}
});
field.getActionMap().put(ACTION_LISTPAGEDOWN, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
completionList.setSelectedIndex(Math.min(completionList.getSelectedIndex() + completionList.getVisibleRowCount(), completionList.getModel().getSize()));
completionList.ensureIndexIsVisible(completionList.getSelectedIndex());
}
});
field.getActionMap().put(ACTION_LISTPAGEUP, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
completionList.setSelectedIndex(Math.max(completionList.getSelectedIndex() - completionList.getVisibleRowCount(), 0));
completionList.ensureIndexIsVisible(completionList.getSelectedIndex());
}
});
field.getActionMap().put(ACTION_FILLIN, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
Object selVal = completionList.getSelectedValue();
if (selVal != null && LOADING.endsWith(selVal.toString())) {
return;
}
field.getDocument().removeDocumentListener(TextValueCompleter.this);
if (selVal != null) {
applyCompletion(selVal.toString());
}
hidePopup();
field.getDocument().addDocumentListener(TextValueCompleter.this);
}
});
field.getActionMap().put(ACTION_HIDEPOPUP, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
hidePopup();
}
});
field.getActionMap().put(ACTION_SHOWPOPUP, new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
buildAndShowPopup();
}
});
}
public TextValueCompleter(Collection<String> completions, JTextField fld, String separators) {
this(completions, fld);
this.separators = separators;
}
public boolean isLoading() {
return loading;
}
public void setLoading(boolean loading) {
this.loading = loading;
if (loading) {
completionListModel.removeAllElements();
completionListModel.addElement(LOADING);
} else {
completionListModel.removeElement(LOADING);
}
}
@NbBundle.Messages("PARTIAL_RESULT=<Result is incomplete, some indices are processed>")
private void buildPopup() {
pattern = Pattern.compile(getCompletionPrefix() + ".+"); //NOI18N
int entryindex = 0;
for (String completion : completions) {
// check if match
Matcher matcher = pattern.matcher(completion);
if (matcher.matches()) {
if (!completionListModel.contains(completion)) {
completionListModel.add(entryindex,
completion);
}
entryindex++;
} else {
completionListModel.removeElement(completion);
}
}
completionListModel.removeElement(Bundle.PARTIAL_RESULT());
if (partial) {
completionListModel.addElement(Bundle.PARTIAL_RESULT());
}
}
private void applyCompletion(String completed) {
field.removeCaretListener(caretListener);
if (separators != null) {
int pos = field.getCaretPosition();
String currentText = field.getText();
int caretPosition=0;
StringTokenizer tok = new StringTokenizer(currentText, separators, true);
int tokens =tok.countTokens();
int count = 0;
String newValue = ""; //NOI18N
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
if (count + token.length() >= pos) {
if (separators.indexOf(token.charAt(0)) != -1) {
newValue = newValue + token;
}
newValue = newValue + completed+separators;
caretPosition=newValue.length();
while (tok.hasMoreTokens()) {
newValue = newValue + tok.nextToken();
}
field.setText(newValue);
field.setCaretPosition(caretPosition);
field.addCaretListener(caretListener);
return;
} else {
count = count + token.length();
newValue = newValue + token;
}
}
newValue = newValue + completed+separators;
field.setText(newValue);
field.setCaretPosition(newValue.length());
} else {
field.setText(completed);
}
field.addCaretListener(caretListener);
}
private String getCompletionPrefix() {
if (separators != null) {
int pos = field.getCaretPosition();
String currentText = field.getText();
StringTokenizer tok = new StringTokenizer(currentText, separators, true);
int count = 0;
String lastToken = ""; //NOI18N
while (tok.hasMoreTokens()) {
String token = tok.nextToken();
if (count + token.length() >= pos) {
if (separators.indexOf(token.charAt(0)) != -1) {
return ""; //NOI18N
}
return Pattern.quote(token.substring(0, pos - count));
} else {
count = count + token.length();
lastToken = token;
}
}
if (lastToken.length() > 0 && separators.indexOf(lastToken.charAt(0)) == -1) {
return Pattern.quote(lastToken);
}
return ""; //NOI18N
} else {
return Pattern.quote(field.getText().trim());
}
}
private void showPopup() {
hidePopup();
if (completionListModel.getSize() == 0) {
return;
}
// figure out where the text field is,
// and where its bottom left is
java.awt.Point los = field.getLocationOnScreen();
int popX = los.x;
int popY = los.y + field.getHeight();
popup = PopupFactory.getSharedInstance().getPopup(field, listScroller, popX, popY);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),ACTION_HIDEPOPUP);
field.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),ACTION_FILLIN);
popup.show();
if (completionList.getSelectedIndex() != -1) {
completionList.ensureIndexIsVisible(completionList.getSelectedIndex());
}
}
private void hidePopup() {
field.getInputMap().remove(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
field.getInputMap().remove(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
if (popup != null) {
popup.hide();
popup = null;
}
}
private class BuildTimer extends Timer {
private static final int DEFAULT_DELAY = 400;
public BuildTimer() {
super(DEFAULT_DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if(!field.isShowing()) {
// became closed in the meantime
return;
}
buildPopup();
showPopup();
}
});
setRepeats(false);
}
};
private final BuildTimer buildTimer = new BuildTimer();
private void buildAndShowPopup() {
buildAndShowPopup(BuildTimer.DEFAULT_DELAY);
}
private void buildAndShowPopup(int delay) {
buildTimer.setInitialDelay(delay);
buildTimer.restart();
}
// DocumentListener implementation
@Override
public void insertUpdate(DocumentEvent e) {
if (field.isFocusOwner()) {
buildAndShowPopup();
}
}
@Override
public void removeUpdate(DocumentEvent e) {
if (field.isFocusOwner() && completionList.isDisplayable() && completionList.isVisible()) {
buildAndShowPopup();
}
}
@Override
public void changedUpdate(DocumentEvent e) {
if (field.isFocusOwner()) {
buildAndShowPopup();
}
}
public void setValueList(Collection<String> values, boolean partial) {
assert SwingUtilities.isEventDispatchThread();
completionListModel.removeAllElements();
completions = values;
this.partial = partial;
if (field.isFocusOwner() && completionList.isDisplayable() && completionList.isVisible()) {
buildAndShowPopup();
}
}
}