/*******************************************************************************
* Breakout Cave Survey Visualizer
*
* Copyright (C) 2014 James Edwards
*
* jedwards8 at fastmail dot fm
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program 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 for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*******************************************************************************/
package org.andork.ui.debug;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.swing.AbstractAction;
import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.JTextComponent;
import javax.swing.text.Keymap;
import org.andork.awt.GridBagWizard;
import org.andork.awt.GridBagWizard.DefaultAutoInsets;
import org.andork.collect.CollectionUtils;
import org.andork.collect.StringTrieMap;
import org.andork.collect.Visitor;
import org.andork.util.ClassFinder;
import org.andork.util.Java7;
@SuppressWarnings("serial")
public class ClassChooserPane extends JPanel {
private static class ClassInfo {
public String fqName;
public String humanFqName;
public String className;
public String location;
public ClassInfo(String fqName) {
this.fqName = fqName;
humanFqName = fqName.replace('$', '.');
int splitPoint = humanFqName.lastIndexOf('.');
if (splitPoint == 0) {
className = humanFqName;
location = "";
} else {
className = humanFqName.substring(splitPoint + 1);
location = humanFqName.substring(0, splitPoint);
}
}
}
private class ClassNameSorter implements Comparator<ClassInfo> {
@Override
public int compare(ClassInfo o1, ClassInfo o2) {
int result = o1.className.compareTo(o2.className);
return result == 0 ? o1.fqName.compareTo(o2.fqName) : result;
}
}
private class Loader implements Runnable {
@Override
public void run() {
ClassFinder.findClasses(new Visitor<String>() {
@Override
public boolean visit(String className) {
if (isAnonymous(className)) {
return true;
}
ClassInfo info = new ClassInfo(className);
if (info.location.length() > 0) {
mapByPackageName.put(info.humanFqName.toLowerCase(), info);
}
mapByClassName.put(info.className.toLowerCase(), info);
return true;
}
});
doSwing(new Runnable() {
@Override
public void run() {
loadingLabel.setVisible(false);
}
});
}
}
@SuppressWarnings("serial")
private class MatchListCellRenderer extends DefaultListCellRenderer {
/**
*
*/
private static final long serialVersionUID = 3028841806077779491L;
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
boolean cellHasFocus) {
ClassInfo info = (ClassInfo) value;
value = "<html>" + info.className + " - <font color=\"gray\">" + info.location + "</font></html>";
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
}
}
private class Updater implements Callable<Object>, Visitor<ClassInfo> {
private class AbortChecker implements Runnable {
boolean abort;
@Override
public void run() {
abort = Java7.Objects.equals(searchString, searchStringForDisplayedMatches);
}
}
final String searchString;
final Set<ClassInfo> matches = new HashSet<ClassInfo>();
protected Updater(String searchString) {
this.searchString = searchString;
}
@Override
public Object call() throws Exception {
AbortChecker checker = new AbortChecker();
doSwing(checker);
if (checker.abort) {
return null;
}
if (!searchString.isEmpty() && !"*".equals(searchString) && !"?".equals(searchString)) {
if (searchString.indexOf('.') >= 0) {
mapByPackageName.get(searchString, this);
} else {
mapByClassName.get(searchString, this);
}
}
final DefaultListModel model = new DefaultListModel();
for (ClassInfo match : CollectionUtils.toSortedArrayList(matches, new ClassNameSorter())) {
model.addElement(match);
}
doSwing(new Runnable() {
@Override
public void run() {
matchList.setModel(model);
if (model.getSize() > 0) {
matchList.setSelectedIndex(0);
}
}
});
return null;
}
@Override
public boolean visit(ClassInfo t) {
matches.add(t);
return !Thread.interrupted();
}
}
/**
*
*/
private static final long serialVersionUID = 927372138232105734L;
private static void doSwing(Runnable r) {
try {
SwingUtilities.invokeAndWait(r);
} catch (InterruptedException e) {
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private static boolean isAnonymous(String className) {
int index = className.indexOf('$');
while (index >= 0 && index < className.length() - 1) {
if (Character.isDigit(className.charAt(index + 1))) {
return true;
}
index = className.indexOf('$', index + 1);
}
return false;
}
public static void main(String[] args) {
final ClassChooserPane pane = new ClassChooserPane();
JOptionPane.showConfirmDialog(null, pane, "Find Type", JOptionPane.OK_CANCEL_OPTION);
System.out.println(pane.getSelectedClassName());
}
JLabel promptLabel;
JTextField typePrefixField;
JLabel matchingItemsLabel;
JLabel loadingLabel;
JList matchList;
JScrollPane matchListScrollPane;
String searchStringForDisplayedMatches;
ThreadPoolExecutor executor;
Future<?> currentFuture;
final StringTrieMap<ClassInfo> mapByPackageName = new StringTrieMap<ClassInfo>();
final StringTrieMap<ClassInfo> mapByClassName = new StringTrieMap<ClassInfo>();
public ClassChooserPane() {
init();
}
public JList getMatchList() {
return matchList;
}
public String getSelectedClassName() {
ClassInfo info = (ClassInfo) matchList.getSelectedValue();
return info == null ? typePrefixField.getText() : info.fqName;
}
private void init() {
promptLabel = new JLabel("Enter type name prefix or pattern (*, ?, or camel case):");
typePrefixField = new JTextField();
matchingItemsLabel = new JLabel("Matching Items:");
loadingLabel = new JLabel("Loading...");
matchList = new JList(new DefaultListModel());
matchList.setCellRenderer(new MatchListCellRenderer());
matchList.setFont(matchList.getFont().deriveFont(Font.PLAIN));
matchList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
matchListScrollPane = new JScrollPane(matchList);
matchListScrollPane.setPreferredSize(new Dimension(600, 400));
GridBagWizard gbw = GridBagWizard.create(this);
gbw.defaults().autoinsets(new DefaultAutoInsets(5, 5));
gbw.put(promptLabel, typePrefixField,
gbw.put(matchingItemsLabel, loadingLabel).intoRow(),
matchListScrollPane).intoColumn();
gbw.put(promptLabel, matchingItemsLabel).west();
gbw.put(loadingLabel).east();
gbw.put(typePrefixField).fillx(1.0);
gbw.put(matchListScrollPane).fillboth(1.0, 1.0);
Keymap kmap = JTextComponent.addKeymap(null, typePrefixField.getKeymap());
kmap.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),
new AbstractAction() {
/**
*
*/
private static final long serialVersionUID = 3599644162474077572L;
@Override
public void actionPerformed(ActionEvent e) {
int selIndex = matchList.getSelectedIndex();
matchList.setSelectedIndex((selIndex + matchList.getModel().getSize() - 1)
% matchList.getModel().getSize());
}
});
kmap.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),
new AbstractAction() {
/**
*
*/
private static final long serialVersionUID = 7913353078046575088L;
@Override
public void actionPerformed(ActionEvent e) {
int selIndex = matchList.getSelectedIndex();
matchList.setSelectedIndex((selIndex + 1) % matchList.getModel().getSize());
}
});
typePrefixField.setKeymap(kmap);
executor = new ThreadPoolExecutor(0, 1, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(ClassChooserPane.class.getSimpleName() + " matcher thread");
thread.setDaemon(true);
return thread;
}
});
typePrefixField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void changedUpdate(DocumentEvent e) {
updateResultsLater();
}
@Override
public void insertUpdate(DocumentEvent e) {
updateResultsLater();
}
@Override
public void removeUpdate(DocumentEvent e) {
updateResultsLater();
}
});
executor.submit(new Loader());
}
public void setTypePrefix(String typePrefix) {
typePrefixField.setText(typePrefix);
}
private void updateResultsLater() {
if (currentFuture != null) {
currentFuture.cancel(true);
}
currentFuture = executor.submit(new Updater(typePrefixField.getText().toLowerCase()));
}
}