package org.skylion.mangareader.util;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.JWindow;
import javax.swing.KeyStroke;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
/**
* A decoration for JTextFields which allow for a dropdown menu to show possible suggestion based off the values
* from an arrayList. Matching is non-strict.
* @author David, Aaron Gokaslan
*/
public class AutoSuggestor {
private final JTextField textField;
private final Window container;
private JPanel suggestionsPanel;
private JWindow autoSuggestionPopUpWindow;
private String typedWord;
private final List<String> dictionary = new ArrayList<>();
private int tW, tH;
private int lastFocusableIndex = 0;
private DocumentListener documentListener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent de) {
checkForAndShowSuggestions();
}
@Override
public void removeUpdate(DocumentEvent de) {
checkForAndShowSuggestions();
}
@Override
public void changedUpdate(DocumentEvent de) {
checkForAndShowSuggestions();
}
};
private final Color suggestionsTextColor;
private final Color suggestionFocusedColor;
public AutoSuggestor(JTextField textField, Window mainWindow, List<String> words, Color popUpBackground, Color textColor, Color suggestionFocusedColor, float opacity) {
this.textField = textField;
this.suggestionsTextColor = textColor;
this.container = mainWindow;
this.suggestionFocusedColor = suggestionFocusedColor;
this.textField.getDocument().addDocumentListener(documentListener);
//WorkAround for anomalous behavior on Macs
//Disables automatic highlighting when focused upon
this.textField.addFocusListener(new FocusListener(){
public void focusGained(FocusEvent e) {
getTextField().getHighlighter().removeAllHighlights();
}
@Override
public void focusLost(FocusEvent e) {
getTextField().getHighlighter().removeAllHighlights();
if(!(e.getOppositeComponent() instanceof Window || e.getOppositeComponent() instanceof SuggestionLabel) && isPopUpVisible()){
showPopUp(false);
}
}
});
this.textField.addHierarchyListener(new HierarchyListener(){//Hides the PopUp if parent is hidden
private Container previousParent = getTextField().getParent();
private ComponentListener cl = new ComponentAdapter(){
@Override
public void componentHidden(ComponentEvent ce){
showPopUp(false);
}
};
@Override
public void hierarchyChanged(HierarchyEvent he) {
if(previousParent != null){
previousParent.removeComponentListener(cl);
}
if(he.getChangedParent() != null){
he.getChangedParent().addComponentListener(cl);
}
previousParent = he.getChangedParent();//Updates the Previous Parent
}
});
mainWindow.addComponentListener(new ComponentAdapter(){
@Override
public void componentResized(ComponentEvent e) {
checkForAndShowSuggestions();
}
@Override
public void componentMoved(ComponentEvent e) {
checkForAndShowSuggestions();
}
});
setDictionary(words);
typedWord = "";
tW = 0;
tH = 0;
autoSuggestionPopUpWindow = new JWindow(mainWindow);
autoSuggestionPopUpWindow.setOpacity(opacity);
suggestionsPanel = new JPanel();
suggestionsPanel.setLayout(new GridLayout(0, 1));
suggestionsPanel.setBackground(popUpBackground);
addKeyBindingToRequestFocusInPopUpWindow();
}
private void addKeyBindingToRequestFocusInPopUpWindow() {
textField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released");
textField.getActionMap().put("Down released", new AbstractAction() {
/**
*
*/
private static final long serialVersionUID = 4304541929636803547L;
@Override
public void actionPerformed(ActionEvent ae) {//focuses the first label on popwindow
resetLabelFocus();
for (int i = 0; i < suggestionsPanel.getComponentCount(); i++) {
if (suggestionsPanel.getComponent(i) instanceof SuggestionLabel) {
((SuggestionLabel) suggestionsPanel.getComponent(i)).setFocused(true);
autoSuggestionPopUpWindow.toFront();
autoSuggestionPopUpWindow.requestFocusInWindow();
suggestionsPanel.requestFocusInWindow();
suggestionsPanel.getComponent(i).requestFocusInWindow();
break;
}
}
}
});
suggestionsPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released");
suggestionsPanel.getActionMap().put("Down released", new AbstractAction() {
/**
* Auto-generated serial long
*/
private static final long serialVersionUID = 7355352173460888575L;
@Override
public void actionPerformed(ActionEvent ae) {//allows scrolling of labels in pop window (I know very hacky for now :))
List<SuggestionLabel> sls = getAddedSuggestionLabels();
int max = sls.size();
if (max > 1) {//more than 1 suggestion
for (int i = 0; i < max; i++) {
SuggestionLabel sl = sls.get(i);
if (sl.isFocused()) {
if (lastFocusableIndex == max - 1) {
lastFocusableIndex = 0;
sl.setFocused(false);
autoSuggestionPopUpWindow.setVisible(false);
setFocusToTextField();
checkForAndShowSuggestions();//fire method as if document listener change occured and fired it
} else {
sl.setFocused(false);
lastFocusableIndex = i;
}
} else if (lastFocusableIndex <= i) {
if (i < max) {
sl.setFocused(true);
autoSuggestionPopUpWindow.toFront();
autoSuggestionPopUpWindow.requestFocusInWindow();
suggestionsPanel.requestFocusInWindow();
suggestionsPanel.getComponent(i).requestFocusInWindow();
lastFocusableIndex = i;
break;
}
}
}
} else {//only a single suggestion was given
autoSuggestionPopUpWindow.setVisible(false);
checkForAndShowSuggestions();//fire method as if document listener change occured and fired it
setFocusToTextField();
}
}
});
suggestionsPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "Up released");
suggestionsPanel.getActionMap().put("Up released", new AbstractAction() {
/**
* Auto-generated serial long
*/
private static final long serialVersionUID = 7355352173460888575L;
@Override
public void actionPerformed(ActionEvent ae) {//allows scrolling of labels in pop window (I know very hacky for now :))
List<SuggestionLabel> sls = getAddedSuggestionLabels();
int max = sls.size();
if (max > 1) {//more than 1 suggestion
for (int i = max-1; i >= 0; i--) {
SuggestionLabel sl = sls.get(i);
if (sl.isFocused()) {
if (lastFocusableIndex == 0) {
sl.setFocused(false);
autoSuggestionPopUpWindow.setVisible(false);
setFocusToTextField();
checkForAndShowSuggestions();//fire method as if document listener change occured and fired it
} else {
sl.setFocused(false);
lastFocusableIndex = i;
}
} else if (lastFocusableIndex >= i) {
if (i >= 0) {
sl.setFocused(true);
autoSuggestionPopUpWindow.toFront();
autoSuggestionPopUpWindow.requestFocusInWindow();
suggestionsPanel.requestFocusInWindow();
suggestionsPanel.getComponent(i).requestFocusInWindow();
lastFocusableIndex = i;
break;
}
}
}
} else {//only a single suggestion was given
autoSuggestionPopUpWindow.setVisible(false);
checkForAndShowSuggestions();//fire method as if document listener change occured and fired it
setFocusToTextField();
}
}
});
}
private void setFocusToTextField() {
container.toFront();
container.requestFocusInWindow();
textField.setFocusable(true);
textField.setRequestFocusEnabled(true);
SwingUtilities.invokeLater(new Runnable() {
void run() {
textField.requestFocusInWindow();
textField.requestFocus();
}
});
}
public List<SuggestionLabel> getAddedSuggestionLabels() {
List<SuggestionLabel> sls = new ArrayList<>();
for (int i = 0; i < suggestionsPanel.getComponentCount(); i++) {
if (suggestionsPanel.getComponent(i) instanceof SuggestionLabel) {
SuggestionLabel sl = (SuggestionLabel) suggestionsPanel.getComponent(i);
sls.add(sl);
}
}
return sls;
}
private void checkForAndShowSuggestions() {
typedWord = getCurrentlyTypedWord();
suggestionsPanel.removeAll();//remove previos words/jlabels that were added
//used to calculate size of JWindow as new Jlabels are added
tW = 0;
tH = 0;
lastFocusableIndex = 0;//Resets the index
boolean added = wordTyped(typedWord);
if (!added) {
if (autoSuggestionPopUpWindow.isVisible()) {
autoSuggestionPopUpWindow.setVisible(false);
}
} else {
showPopUpWindow();
setFocusToTextField();
}
}
protected void addWordToSuggestions(String word) {
SuggestionLabel suggestionLabel = new SuggestionLabel(word, suggestionFocusedColor, suggestionsTextColor, this);
calculatePopUpWindowSize(suggestionLabel);
suggestionsPanel.add(suggestionLabel);
}
/**
* Gets the String currently used. Can be modified to show suggestions for each word.
* @return
*/
public String getCurrentlyTypedWord() {
String text = textField.getText();
return text;
}
private void calculatePopUpWindowSize(JLabel label) {
//so we can size the JWindow correctly
if (tW < label.getPreferredSize().width) {
tW = label.getPreferredSize().width;
}
tH += label.getPreferredSize().height;
}
private void showPopUpWindow() {
autoSuggestionPopUpWindow.getContentPane().add(suggestionsPanel);
autoSuggestionPopUpWindow.setMinimumSize(new Dimension(textField.getWidth() -
- textField.getInsets().left - textField.getInsets().right, 30));
autoSuggestionPopUpWindow.setSize(tW, tH);
autoSuggestionPopUpWindow.setVisible(true);
//Calculates the optimal window location
int windowX = container.getX() + container.getInsets().left + textField.getX() + textField.getMargin().left + 3;
int windowY = container.getY() + container.getInsets().top - textField.getInsets().bottom + textField.getY() + textField.getHeight();
autoSuggestionPopUpWindow.setLocation((windowX), windowY);
autoSuggestionPopUpWindow.setMinimumSize(new Dimension(textField.getWidth() - textField.getInsets().right, 30));
autoSuggestionPopUpWindow.setSize(textField.getWidth() - textField.getInsets().right, autoSuggestionPopUpWindow.getHeight());
autoSuggestionPopUpWindow.revalidate();
autoSuggestionPopUpWindow.repaint();
setFocusToTextField();
}
public void setDictionary(List<String> words) {
dictionary.clear();
if (words == null) {
return;//so we can call constructor with null value for dictionary without exception thrown
}
//Converts List<String> to remove duplicates
Set<String> s = new LinkedHashSet<String>(words);
dictionary.addAll(s);
Collections.sort(dictionary);//The List works much better when sorted
}
public List<String> getDictionary(){
return dictionary;
}
public JWindow getAutoSuggestionPopUpWindow() {
return autoSuggestionPopUpWindow;
}
public Window getContainer() {
return container;
}
public JTextField getTextField() {
return textField;
}
public void addToDictionary(String word) {
dictionary.add(word);
}
boolean wordTyped(String typedWord) {
if (typedWord.isEmpty()) {
return false;
}
//System.out.println("Typed word: " + typedWord);
boolean suggestionAdded = false;
for (String word : dictionary) {//get words in the dictionary which we added
boolean fullymatches = true;
if(typedWord.length()<=word.length()){//Important! Removes harmful exception for words not in list
for (int i = 0; i < typedWord.length(); i++) {//each string in the word
if (!typedWord.toLowerCase().startsWith(String.valueOf(word.toLowerCase().charAt(i)), i)) {//check for match
fullymatches = false;
break;
}
}
}
else{
fullymatches = false;
}
if (fullymatches) {
if(tH>container.getHeight()-container.getInsets().top-container.getInsets().bottom-
textField.getY() - textField.getHeight()){
break;//Prevents the suggestions panel from drawing unused suggestionLabels offscreen
}
addWordToSuggestions(word);
suggestionAdded = true;
}
}
return suggestionAdded;
}
public boolean isPopUpVisible(){
return this.autoSuggestionPopUpWindow.isVisible();
}
public void showPopUp(boolean show){
this.autoSuggestionPopUpWindow.setVisible(show);
}
protected void setSelectedSuggestionLabel(SuggestionLabel sl){
List<SuggestionLabel> sls = getAddedSuggestionLabels();
if(lastFocusableIndex<sls.size() && (sl == null || sls.contains(sl))){
sls.get(lastFocusableIndex).setFocused(false);//Resets the index
}
if(!sls.contains(sl)){
return;
}
sl.setFocused(true);
lastFocusableIndex = sls.indexOf(sl);
}
private void resetLabelFocus(){
getAddedSuggestionLabels().get(lastFocusableIndex).setFocused(false);
lastFocusableIndex = 0;
}
}
class SuggestionLabel extends JLabel {
/**
* Auto-generated serial long
*/
private static final long serialVersionUID = 1L;
private boolean focused = false;
private final JWindow autoSuggestionsPopUpWindow;
private final JTextField textField;
private final AutoSuggestor autoSuggestor;
private Color suggestionsTextColor, suggestionBorderColor;
public SuggestionLabel(String string, final Color borderColor, Color suggestionsTextColor, AutoSuggestor autoSuggestor) {
super(string);
this.suggestionsTextColor = suggestionsTextColor;
this.autoSuggestor = autoSuggestor;
this.textField = autoSuggestor.getTextField();
this.suggestionBorderColor = borderColor;
this.autoSuggestionsPopUpWindow = autoSuggestor.getAutoSuggestionPopUpWindow();
initComponent();
}
private void initComponent() {
setFocusable(true);
setForeground(suggestionsTextColor);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent me) {
replaceWithSuggestedText();
autoSuggestionsPopUpWindow.setVisible(false);
fireTextFieldActionEvents();
}
@Override
public void mouseEntered(MouseEvent me) {
autoSuggestor.setSelectedSuggestionLabel(SuggestionLabel.this);//Highlights selected label
}
});
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, true), "Enter released");
getActionMap().put("Enter released", new AbstractAction() {
/**
* Auto-generated serial long
*/
private static final long serialVersionUID = 2010022788442417401L;
@Override
public void actionPerformed(ActionEvent ae) {
replaceWithSuggestedText();
autoSuggestionsPopUpWindow.setVisible(false);
fireTextFieldActionEvents();
}
});
}
public void setFocused(boolean focused) {
if (focused) {
setBorder(new LineBorder(suggestionBorderColor));
} else {
setBorder(null);
}
repaint();
this.focused = focused;
}
public boolean isFocused() {
return focused;
}
private void replaceWithSuggestedText() {
String suggestedWord = getText();
String text = textField.getText();
String typedWord = autoSuggestor.getCurrentlyTypedWord();
if(text.indexOf(typedWord)==-1){
return;
}
String t = text.substring(0, text.lastIndexOf(typedWord));
String tmp = t + text.substring(text.lastIndexOf(typedWord)).replace(typedWord, suggestedWord);
textField.setText(tmp + " ");
}
private void fireTextFieldActionEvents(){
int uniqueId = (int) System.currentTimeMillis();
String commandName = "Word Replaced";
for(ActionListener tmp: textField.getActionListeners()){//Manually fires action events.
tmp.actionPerformed(new ActionEvent(textField, uniqueId, commandName));
}
}
}