package org.esa.snap.ui.tooladapter.model;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Extension of JTextArea that allows autocompletion of entries, based on
* operator's system variables and parameters.
*
* @author Cosmin Cara
*/
public class AutoCompleteTextArea extends JTextArea {
private InputOptionsPanel suggestion;
private List<String> autoCompleteEntries;
private char triggerChar;
public AutoCompleteTextArea(String text, int rows, int columns) {
super(text, rows, columns);
addKeyListener(new KeyListener() {
private boolean triggerCharPressed;
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_ENTER ||
e.getKeyChar() == KeyEvent.VK_TAB ||
e.getKeyChar() == KeyEvent.VK_SPACE) {
if (suggestion != null && (triggerCharPressed || suggestion.isVisible())) {
if (suggestion.insertSelection()) {
//e.consume();
final int position = getCaretPosition();
SwingUtilities.invokeLater(() -> {
try {
getDocument().remove(position - 1, 1);
} catch (BadLocationException ex) {
ex.printStackTrace();
}
});
}
}
triggerCharPressed = false;
}
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_DOWN && suggestion != null && triggerCharPressed) {
suggestion.moveDown();
} else if (e.getKeyCode() == KeyEvent.VK_UP && suggestion != null && triggerCharPressed) {
suggestion.moveUp();
} else if (e.getKeyChar() == triggerChar) {
triggerCharPressed = true;
SwingUtilities.invokeLater(AutoCompleteTextArea.this::showSuggestion);
} else if (Character.isLetterOrDigit(e.getKeyChar()) && triggerCharPressed || e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
SwingUtilities.invokeLater(AutoCompleteTextArea.this::showSuggestion);
} else if (Character.isWhitespace(e.getKeyChar()) || e.getKeyCode() == KeyEvent.VK_ESCAPE) {
triggerCharPressed = false;
hideSuggestion();
}
}
@Override
public void keyPressed(KeyEvent e) {
}
});
}
/**
* Sets the character that will trigger autocompletion
*/
public void setTriggerChar(char trigger) {
triggerChar = trigger;
}
/**
* Sets the list of autocompletion entries (suggestions)
*/
public void setAutoCompleteEntries(List<String> entries) {
autoCompleteEntries = entries;
}
protected void showSuggestion() {
hideSuggestion();
final int position = getCaretPosition();
Point location;
try {
location = modelToView(position).getLocation();
} catch (BadLocationException e) {
return;
}
String text = getText();
int start = Math.max(0, text.lastIndexOf(triggerChar, position));
if (start + 1 > position) {
return;
}
final String subWord = text.substring(start + 1, position);
if (suggestion == null) {
suggestion = new InputOptionsPanel(this);
}
List<String> filtered = autoCompleteEntries != null ? autoCompleteEntries.stream().filter(e -> e.startsWith(subWord)).collect(Collectors.toList()) :
(autoCompleteEntries = new ArrayList<>());
if (filtered.isEmpty()) {
hideSuggestion();
} else {
suggestion.setSuggestionList(subWord.isEmpty() ? autoCompleteEntries : filtered, subWord);
suggestion.show(position, location);
SwingUtilities.invokeLater(this::requestFocusInWindow);
}
}
protected void hideSuggestion() {
if (suggestion != null) {
suggestion.hide();
}
}
/**
* This will visually hold the current suggestions (if any).
*/
class InputOptionsPanel {
private JList<String> list;
private JPopupMenu popupMenu;
private String subWord;
private int insertionPosition;
private final JTextArea textArea;
public InputOptionsPanel(JTextArea parent) {
popupMenu = new JPopupMenu();
popupMenu.setOpaque(false);
popupMenu.setBorder(null);
textArea = parent;
}
public void hide() {
popupMenu.setVisible(false);
}
public void show(int position, Point location) {
this.insertionPosition = position;
popupMenu.show(textArea, location.x, textArea.getBaseline(0, 0) + location.y);
}
public boolean isVisible() { return popupMenu.isVisible(); }
public void setSuggestionList(List<String> entries, String subWord) {
popupMenu.removeAll();
this.subWord = subWord;
createSuggestionList(entries);
popupMenu.add(list, BorderLayout.CENTER);
}
public boolean insertSelection() {
if (list.getSelectedValue() != null) {
String text = textArea.getText();
try {
final String selectedSuggestion = list.getSelectedValue();
Document document = textArea.getDocument();
int insertIndex = text.lastIndexOf(subWord, insertionPosition);
document.remove(insertIndex, subWord.length());
document.insertString(insertIndex, selectedSuggestion, null);
return true;
} catch (BadLocationException ignored) {
textArea.setText(text);
}
hide();
}
return false;
}
public void moveUp() {
int index = Math.max(list.getSelectedIndex() - 1, 0);
selectIndex(index);
}
public void moveDown() {
int index = Math.min(list.getSelectedIndex() + 1, list.getModel().getSize() - 1);
selectIndex(index);
}
private void createSuggestionList(List<String> entries) {
if (list == null) {
list = new JList<>(entries.toArray(new String[entries.size()]));
list.setDoubleBuffered(true);
list.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY, 0));
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
insertSelection();
}
}
});
} else {
list.removeAll();
list.setListData(entries.toArray(new String[entries.size()]));
}
list.setSelectedIndex(0);
}
private void selectIndex(int index) {
final int position = textArea.getCaretPosition();
list.setSelectedIndex(index);
SwingUtilities.invokeLater(() -> textArea.setCaretPosition(position));
}
}
}