org.praxislive.ide.pxr.graph.JSuggestField Maven / Gradle / Ivy
The newest version!
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2019 Neil C Smith / David von Ah
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 3 for more details.
*
* You should have received a copy of the GNU General Public License version 3
* along with this work; if not, see http://www.gnu.org/licenses/
*
*
* Please visit https://www.praxislive.org if you need additional information or
* have any questions.
*/
package org.praxislive.ide.pxr.graph;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.HierarchyBoundsAdapter;
import java.awt.event.HierarchyEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Iterator;
import java.util.Locale;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.SwingUtilities;
class JSuggestField extends JTextField {
/**
* unique ID for serialization
*/
private static final long serialVersionUID = 1756202080423312153L;
private Popup popup;
/**
* List contained in the drop-down dialog.
*/
private JList list;
/**
* Vectors containing the original data and the filtered data for the
* suggestions.
*/
private Vector data, suggestions;
/**
* Separate matcher-thread, prevents the text-field from hanging while the
* suggestions are beeing prepared.
*/
private InterruptableMatcher matcher;
/**
* Needed for the new narrowing search, so we know when to reset the list
*/
private String lastWord = "";
/**
* Create a new JSuggestField.
*
*/
public JSuggestField() {
data = new Vector();
suggestions = new Vector();
addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
@Override
public void ancestorMoved(HierarchyEvent e) {
hideSuggest();
}
@Override
public void ancestorResized(HierarchyEvent e) {
hideSuggest();
}
});
addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
hideSuggest();
}
});
list = new JList();
list.setFocusable(false);
list.addMouseListener(new MouseAdapter() {
private int selected;
@Override
public void mouseReleased(MouseEvent e) {
// if (selected == list.getSelectedIndex()) {
// provide double-click for selecting a suggestion
setText((String) list.getSelectedValue());
fireActionPerformed();
hideSuggest();
// }
// selected = list.getSelectedIndex();
}
});
addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
return;
}
if (e.getKeyCode() == KeyEvent.VK_DOWN) {
if (isSuggestVisible()) {
list.setSelectedIndex(list.getSelectedIndex() + 1);
list.ensureIndexIsVisible(list.getSelectedIndex() + 1);
setText((String) list.getSelectedValue());
return;
} else {
showPopup();
}
return;
} else if (e.getKeyCode() == KeyEvent.VK_UP) {
if (isSuggestVisible()) {
list.setSelectedIndex(list.getSelectedIndex() - 1);
list.ensureIndexIsVisible(list.getSelectedIndex() - 1);
setText((String) list.getSelectedValue());
} else {
showPopup();
}
return;
} else if ((e.getKeyCode() == KeyEvent.VK_ENTER
|| e.getKeyCode() == KeyEvent.VK_TAB)
&& list.getSelectedIndex() != -1
&& suggestions.size() > 0) {
setText((String) list.getSelectedValue());
hidePopup();
return;
}
showSuggest();
}
@Override
public void keyReleased(KeyEvent e) {
}
});
}
/**
* Create a new JSuggestField.
*
* @param owner Frame containing this JSuggestField
* @param data Available suggestions
*/
public JSuggestField(Vector data) {
this();
setSuggestData(data);
}
/**
* Sets new data used to suggest similar words.
*
* @param data Vector containing available words
* @return success, true unless the data-vector was null
*/
public boolean setSuggestData(Vector data) {
if (data == null) {
return false;
}
// Collections.sort(data);
this.data = data;
list.setListData(data);
suggestions.clear();
return true;
}
/**
* Get all words that are available for suggestion.
*
* @return Vector containing Strings
*/
@SuppressWarnings("unchecked")
public Vector getSuggestData() {
return (Vector) data.clone();
}
/**
* Force the suggestions to be displayed (Useful for buttons e.g. for using
* JSuggestionField like a ComboBox)
*/
public void showSuggest() {
if (!getText().toLowerCase().contains(lastWord.toLowerCase())) {
suggestions.clear();
}
if (suggestions.isEmpty()) {
suggestions.addAll(data);
}
if (matcher != null) {
matcher.stop = true;
}
matcher = new InterruptableMatcher();
SwingUtilities.invokeLater(matcher);
lastWord = getText();
}
/**
* Force the suggestions to be hidden (Useful for buttons, e.g. to use
* JSuggestionField like a ComboBox)
*/
public void hideSuggest() {
hidePopup();
}
private void showPopup() {
if (popup != null) {
return;
}
JScrollPane scroll = new JScrollPane(list,
JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
Dimension dim = new Dimension(getSize().width,
Math.min(12, data.size()) *
(list.getCellRenderer().getListCellRendererComponent(list, "XXX", 0, true, true)
.getPreferredSize().height + 4));
scroll.setPreferredSize(dim);
Point loc = getLocationOnScreen();
GraphicsConfiguration gc = getGraphicsConfiguration();
Rectangle screenBounds = gc.getBounds();
Insets screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc);
int screenHeight = screenBounds.height - screenInsets.bottom;
if (loc.y + getHeight() + dim.getHeight() > screenHeight) {
loc.y -= dim.getHeight();
} else {
loc.y += getHeight();
}
popup = PopupFactory.getSharedInstance().getPopup(this, scroll, loc.x, loc.y);
popup.show();
}
private void hidePopup() {
if (popup != null) {
popup.hide();
popup = null;
}
}
private boolean isSuggestVisible() {
return popup != null;
}
private class InterruptableMatcher implements Runnable {
private boolean stop;
@Override
public void run() {
if (stop || !JSuggestField.this.isVisible()) {
return;
}
try {
String word = getText().toLowerCase(Locale.ROOT);
suggestions.clear();
data.forEach(datum -> {
if (datum.toLowerCase(Locale.ROOT).startsWith(word)) {
suggestions.add(datum);
}
});
data.forEach(datum -> {
String lower = datum.toLowerCase(Locale.ROOT);
if (!lower.startsWith(word) && lower.contains(word)) {
suggestions.add(datum);
}
});
if (suggestions.size() > 0) {
list.setListData(suggestions);
showPopup();
list.setSelectedIndex(0);
list.ensureIndexIsVisible(0);
} else {
hidePopup();
}
} catch (Exception ex) {
// Despite all precautions, external changes have occurred.
// Let the new thread handle it...
Logger.getLogger(JSuggestField.class.getName()).log(Level.WARNING, "Error in matcher", ex);
}
}
}
@Override
protected void fireActionPerformed() {
if (matcher != null) {
matcher.stop = true;
matcher = null;
}
super.fireActionPerformed();
}
}