/* *****************************************************************************
* Compilation: javac-algs4 AutocompleteGUI.java
* Execution: java-algs4 AutocompleteGUI input.txt k
* Dependencies: Autocomplete.java Term.java
*
* @author Matthew Drabick
* @author Ming-Yee Tsang
* @author Andrew Ward
* @author Kevin Wayne
*
* Interactive GUI used to demonstrate the Autocomplete data type.
*
* * Reads a list of terms and weights from a file, specified as a
* command-line argument.
*
* * As the user types in a text box, display the top-k terms
* that start with the text that the user types.
*
* * Displays the result in a browser if the user selects a term
* (by pressing enter, clicking a selection, or pressing the
* "Search Google" button).
*
*
* BUG: Search bar and suggestion drop-down don't resize properly with window;
* they stay the same size when the window gets wider, and the weights
* get hidden when the window gets smaller.
*
* FEATURE: make weights be in left column instead of right column ?
* (to match toString() and output format of test client on assignment)
*
* NOTE: Requires Java 7 (or above) because JList was not retrofitted to support
* generics until Java 7.
*
*
* % java-algs4 AutocompleteGUI cities.txt 10
*
**************************************************************************** */
import edu.princeton.cs.algs4.In;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.LayoutStyle;
import javax.swing.ListSelectionModel;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.MouseInputAdapter;
import java.awt.Color;
import java.awt.Container;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
public class AutocompleteGUI extends JFrame {
// for serializable classes
private static final long serialVersionUID = 1L;
private static final int DEF_WIDTH = 850; // width of the GUI window
private static final int DEF_HEIGHT = 400; // height of the GUI window
// URL prefix for searches
private static final String SEARCH_URL = "https://www.google.com/search?q=";
// Display top k results
private final int k;
// Indicates whether to display weights next to query matches
private boolean displayWeights = true;
/**
* Initializes the GUI, and the associated Autocomplete object
*
* @param filename the file to read all the autocomplete data from
* @param k the maximum number of suggestions to return
*/
public AutocompleteGUI(String filename, int k) {
this.k = k;
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setTitle("Autocomplete Me");
setPreferredSize(new Dimension(DEF_WIDTH, DEF_HEIGHT));
pack();
setLocationRelativeTo(null);
Container content = getContentPane();
GroupLayout layout = new GroupLayout(content);
content.setLayout(layout);
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
final AutocompletePanel ap = new AutocompletePanel(filename);
JLabel textLabel = new JLabel("Search query:");
textLabel.setLabelFor(ap);
// Create and add a listener to the Search button
JButton searchButton = new JButton("Search Google");
searchButton.addActionListener(
ae -> searchOnline(ap.getSelectedText()));
// Create and add a listener to a "Show weights" checkbox
JCheckBox checkbox = new JCheckBox("Show weights", null, displayWeights);
checkbox.addActionListener(
ae -> {
displayWeights = !displayWeights;
ap.update();
});
// Define the layout of the window
layout.setHorizontalGroup(
layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(
GroupLayout.Alignment.TRAILING)
.addComponent(textLabel, GroupLayout.PREFERRED_SIZE,
GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE)
.addComponent(checkbox, GroupLayout.PREFERRED_SIZE,
GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE))
.addPreferredGap(LayoutStyle.ComponentPlacement.RELATED,
GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE)
.addComponent(ap, 0, GroupLayout.DEFAULT_SIZE, DEF_WIDTH)
.addComponent(searchButton, GroupLayout.PREFERRED_SIZE,
GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE)
);
layout.setVerticalGroup(
layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(
GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(textLabel)
.addComponent(checkbox))
.addComponent(ap)
.addComponent(searchButton))
);
}
/**
* The panel that interfaces with the Autocomplete object. It consists
* of a search bar that text can be entered into, and a drop-down list
* of suggestions auto-completing the user's query.
*/
private class AutocompletePanel extends JPanel {
// for serializable classes
private static final long serialVersionUID = 1L;
private static final int BOTTOM_MARGIN = 5; // extra room to leave at the bottom
private final JTextField searchText; // the search bar
private Autocomplete auto; // the Autocomplete object
private String[] results = new String[k]; // an array of matches
private JList<String> suggestions; // a list of autocomplete matches
private JScrollPane scrollPane; // the scroll bar on the side of the
private JPanel suggestionsPanel; // the dropdown menu of suggestions
// the suggestion drop-down below the
// last suggestion
// change how this is implemented so it is dynamic;
// shouldn't have to define a column number.
// Keep these next two values in sync! - used to keep the search box
// the same width as the drop-down
// DEF_COLUMNS should be the number of characters in suggListLen
// number of columns in the search text that is kept
private static final int DEF_COLUMNS = 45;
// an example of one of the longest strings in the database
private static final String suggListLen =
"<b>Harry Potter and the Deathly Hallows: Part 1 (2010)</b>";
/**
* Creates the Autocomplete object and the search bar and suggestion
* drop-down portions of the GUI
*
* @param filename the file the Autocomplete object is constructed from
*/
public AutocompletePanel(String filename) {
super();
// Read in the data
Term[] terms;
try {
In in = new In(filename);
String line0 = in.readLine();
if (line0 == null) {
throw new IllegalArgumentException("Could not read line 0 of " + filename);
}
int n = Integer.parseInt(line0);
terms = new Term[n];
for (int i = 0; i < n; i++) {
String line = in.readLine();
if (line == null) {
throw new IllegalArgumentException(
"Could not read line " + (i + 1) + " of " + filename);
}
int tab = line.indexOf('\t');
if (tab == -1) {
throw new IllegalArgumentException(
"No tab character in line " + (i + 1) + " of " + filename);
}
long weight = Long.parseLong(line.substring(0, tab).trim());
String query = line.substring(tab + 1);
terms[i] = new Term(query, weight);
}
}
catch (Exception e) {
throw new IllegalArgumentException(
"Could not read or parse input file " + filename);
}
// Create the autocomplete object
auto = new Autocomplete(terms);
GroupLayout layout = new GroupLayout(this);
this.setLayout(layout);
// create the search text, and allow the user to interact with it
searchText = new JTextField(DEF_COLUMNS);
searchText.setMaximumSize(new Dimension(
searchText.getMaximumSize().width,
searchText.getPreferredSize().height));
searchText.getInputMap().put(KeyStroke.getKeyStroke("UP"), "none");
searchText.getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "none");
searchText.addFocusListener(
new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
int pos = searchText.getText().length();
searchText.setCaretPosition(pos);
}
public void focusLost(FocusEvent e) {
}
});
// create the search text box
JPanel searchTextPanel = new JPanel();
searchTextPanel.add(searchText);
searchTextPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
searchTextPanel.setLayout(new GridLayout(1, 1));
// create the drop-down menu items
int fontsize = 13;
int cellHeight = 20;
suggestions = new JList<String>(results);
suggestions.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
suggestions.setVisible(false);
suggestions.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
suggestions.setMaximumSize(new Dimension(
searchText.getMaximumSize().width,
suggestions.getPreferredSize().height));
// Set to make equal to the width of the textfield
suggestions.setPrototypeCellValue(suggListLen);
suggestions.setFont(
suggestions.getFont().deriveFont(Font.PLAIN, fontsize));
suggestions.setFixedCellHeight(cellHeight);
// add arrow-key interactivity to the drop-down menu items
Action makeSelection = new AbstractAction() {
// for serializable classes
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
if (!suggestions.isSelectionEmpty()) {
String selection = suggestions.getSelectedValue();
if (displayWeights)
selection = selection.substring(
0, selection.indexOf("<td width="));
selection = selection.replaceAll("\\<.*?>", "");
searchText.setText(selection);
getSuggestions(selection);
}
searchOnline(searchText.getText());
}
};
Action moveSelectionUp = new AbstractAction() {
// for serializable classes
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
if (suggestions.getSelectedIndex() >= 0) {
suggestions.requestFocusInWindow();
suggestions.setSelectedIndex(suggestions.getSelectedIndex() - 1);
}
}
};
Action moveSelectionDown = new AbstractAction() {
// for serializable classes
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
if (suggestions.getSelectedIndex() != results.length) {
suggestions.requestFocusInWindow();
suggestions.setSelectedIndex(suggestions.getSelectedIndex() + 1);
}
}
};
Action moveSelectionUpFocused = new AbstractAction() {
// for serializable classes
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
if (suggestions.getSelectedIndex() == 0) {
suggestions.clearSelection();
searchText.requestFocusInWindow();
searchText.setSelectionEnd(0);
}
else if (suggestions.getSelectedIndex() >= 0) {
suggestions.setSelectedIndex(suggestions.getSelectedIndex() - 1);
}
}
};
suggestions.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
KeyStroke.getKeyStroke("UP"), "moveSelectionUp");
suggestions.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
KeyStroke.getKeyStroke("DOWN"), "moveSelectionDown");
suggestions.getInputMap(JComponent.WHEN_FOCUSED).put(
KeyStroke.getKeyStroke("ENTER"), "makeSelection");
suggestions.getInputMap().put(
KeyStroke.getKeyStroke("UP"), "moveSelectionUpFocused");
suggestions.getActionMap().put("moveSelectionUp", moveSelectionUp);
suggestions.getActionMap().put("moveSelectionDown", moveSelectionDown);
suggestions.getActionMap().put("moveSelectionUpFocused", moveSelectionUpFocused);
suggestions.getActionMap().put("makeSelection", makeSelection);
// Create the suggestion drop-down panel and scroll bar
suggestionsPanel = new JPanel();
scrollPane = new JScrollPane(suggestions);
scrollPane.setVisible(false);
int prefBarWidth = scrollPane.getVerticalScrollBar().getPreferredSize().width;
suggestions.setPreferredSize(new Dimension(searchText.getPreferredSize().width, 0));
scrollPane.setAutoscrolls(true);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
// resize widths and heights of all components to fit nicely
int preferredWidth = searchText.getPreferredSize().width + 2 * prefBarWidth;
int maxWidth = searchText.getMaximumSize().width + 2 * prefBarWidth;
int searchBarHeight = searchText.getPreferredSize().height;
int suggestionHeight = suggestions.getFixedCellHeight();
int maxSuggestionHeight = DEF_HEIGHT * 2;
suggestionsPanel.setPreferredSize(new Dimension(preferredWidth, suggestionHeight));
suggestionsPanel.setMaximumSize(new Dimension(maxWidth, maxSuggestionHeight));
suggestionsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
suggestionsPanel.add(scrollPane);
suggestionsPanel.setLayout(new GridLayout(1, 1));
this.setPreferredSize(new Dimension(preferredWidth, this.getPreferredSize().height));
this.setMaximumSize(
new Dimension(preferredWidth, searchBarHeight + maxSuggestionHeight));
searchTextPanel.setPreferredSize(new Dimension(preferredWidth, searchBarHeight));
searchTextPanel.setMaximumSize(new Dimension(maxWidth, searchBarHeight));
searchText.setMaximumSize(new Dimension(maxWidth, searchBarHeight));
// add mouse interactivity with the drop-down menu
suggestions.addMouseListener(
new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount() >= 1) {
int index = suggestions.locationToIndex(mouseEvent.getPoint());
if (index >= 0) {
String selection = getSelectedText();
searchText.setText(selection);
String text = searchText.getText();
getSuggestions(text);
searchOnline(searchText.getText());
}
}
}
@Override
public void mouseEntered(MouseEvent mouseEvent) {
int index = suggestions.locationToIndex(mouseEvent.getPoint());
suggestions.requestFocusInWindow();
suggestions.setSelectedIndex(index);
}
@Override
public void mouseExited(MouseEvent mouseEvent) {
suggestions.clearSelection();
searchText.requestFocusInWindow();
}
});
suggestions.addMouseMotionListener(
new MouseInputAdapter() {
@Override
// Google a term when a user clicks on the dropdown menu
public void mouseClicked(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount() >= 1) {
int index = suggestions.locationToIndex(mouseEvent.getPoint());
if (index >= 0) {
String selection = getSelectedText();
searchText.setText(selection);
String text = searchText.getText();
getSuggestions(text);
searchOnline(searchText.getText());
}
}
}
@Override
public void mouseEntered(MouseEvent mouseEvent) {
int index = suggestions.locationToIndex(mouseEvent.getPoint());
suggestions.requestFocusInWindow();
suggestions.setSelectedIndex(index);
}
@Override
public void mouseMoved(MouseEvent mouseEvent) {
int index = suggestions.locationToIndex(mouseEvent.getPoint());
suggestions.requestFocusInWindow();
suggestions.setSelectedIndex(index);
}
});
// add a listener that allows updates each time the user types
searchText.getDocument().addDocumentListener(
new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
changedUpdate(e);
}
public void removeUpdate(DocumentEvent e) {
changedUpdate(e);
}
public void changedUpdate(DocumentEvent e) {
String text = searchText.getText();
// updates the drop-down menu
getSuggestions(text);
updateListSize();
}
});
// When a user clicks on a suggestion, Google it
searchText.addActionListener(
e -> {
String selection = getSelectedText();
searchText.setText(selection);
getSuggestions(selection);
searchOnline(searchText.getText());
});
// Define the layout of the text box and suggestion dropdown
layout.setHorizontalGroup(
layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(
GroupLayout.Alignment.LEADING)
.addComponent(searchTextPanel, 0,
GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE)
.addComponent(suggestionsPanel,
GroupLayout.DEFAULT_SIZE,
GroupLayout.DEFAULT_SIZE,
GroupLayout.PREFERRED_SIZE))
);
layout.setVerticalGroup(
layout.createSequentialGroup()
.addComponent(searchTextPanel)
.addComponent(suggestionsPanel)
);
}
/**
* Re-populates the drop-down menu with the new suggestions, and
* resizes the containing panel vertically
*/
private void updateListSize() {
int rows = k;
if (suggestions.getModel().getSize() < k) {
rows = suggestions.getModel().getSize();
}
int suggWidth = searchText.getPreferredSize().width;
int suggPanelWidth = suggestionsPanel.getPreferredSize().width;
int suggHeight = rows * suggestions.getFixedCellHeight();
suggestions.setPreferredSize(new Dimension(suggWidth, suggHeight));
suggestionsPanel
.setPreferredSize(new Dimension(suggPanelWidth, suggHeight + BOTTOM_MARGIN));
suggestionsPanel
.setMaximumSize(new Dimension(suggPanelWidth, suggHeight + BOTTOM_MARGIN));
// redraw the suggestion panel
suggestionsPanel.setVisible(false);
suggestionsPanel.setVisible(true);
}
// see getSuggestions for documentation
public void update() {
getSuggestions(searchText.getText());
}
/**
* Makes a call to the implementation of Autocomplete to get
* suggestions for the currently entered text.
*
* @param text string to search for
*/
public void getSuggestions(String text) {
// don't search for suggestions if there is no input
if (text.equals("")) {
suggestions.setListData(new String[0]);
suggestions.clearSelection();
suggestions.setVisible(false);
scrollPane.setVisible(false);
}
else {
// get all matching terms
Term[] allResults = auto.allMatches(text);
if (allResults == null) {
throw new NullPointerException("allMatches() is null");
}
results = new String[Math.min(k, allResults.length)];
if (Math.min(k, allResults.length) > 0) {
for (int i = 0; i < results.length; i++) {
// A bit of a hack to get the Term's query string
// and weight from toString()
String next = allResults[i].toString();
if (allResults[i] == null) {
throw new NullPointerException("allMatches() "
+ "returned an array with a null entry");
}
int tab = next.indexOf('\t');
if (tab < 0) {
throw new RuntimeException("allMatches() returned"
+ " an array with an entry without a tab:"
+ " '" + next + "'");
}
// truncate length if needed
String query = next.substring(tab);
if (query.length() > suggListLen.length())
query = query.substring(0, suggListLen.length());
// create the table HTML
int textLen = text.length();
results[i] = "<html><table width=\""
+ searchText.getPreferredSize().width + "\">"
+ "<tr><td align=left>"
+ query.substring(0, textLen + 1)
+ "<b>" + query.substring(textLen + 1) + "</b>";
if (displayWeights) {
String weight = next.substring(0, tab).trim();
results[i] += "<td width=\"10%\" align=right>"
+ "<font size=-1><span id=\"weight\" "
+ "style=\"float:right;color:gray\">"
+ weight + "</font>";
}
results[i] += "</table></html>";
}
suggestions.setListData(results);
suggestions.setVisible(true);
scrollPane.setVisible(true);
}
else {
// No suggestions
suggestions.setListData(new String[0]);
suggestions.clearSelection();
suggestions.setVisible(false);
scrollPane.setVisible(false);
}
}
}
// bring the clicked suggestion up to the Search bar and search it
public String getSelectedText() {
if (!suggestions.isSelectionEmpty()) {
String selection = suggestions.getSelectedValue();
if (displayWeights) {
selection = selection.substring(0, selection.indexOf("<td width="));
}
selection = selection.replaceAll("\\<.*?>", "");
selection = selection.replaceAll("^[ \t]+|[ \t]+$", "");
return selection;
}
else {
return getSearchText();
}
}
public String getSearchText() {
return searchText.getText();
}
}
/**
* Creates a URI from the user-defined string and searches the web with the
* selected search engine
* Opens the default web browser (or a new tab if it is already open)
*
* @param s string to search online for
*/
private void searchOnline(String s) {
try {
URI tempAddress = new URI(SEARCH_URL + URLEncoder.encode(s.trim(), "UTF-8"));
URI searchAddress = new URI(tempAddress.toASCIIString()); // Hack to handle Unicode
Desktop.getDesktop().browse(searchAddress);
}
catch (URISyntaxException | IOException e) {
throw new RuntimeException("could not open " + s + " in browser");
}
}
/**
* Creates an AutocompleteGUI object and start it continuously running
*
* @param args the filename from which the Autocomplete object is populated
* and the integer k which defines the maximum number of objects in the
* dropdown menu
*/
public static void main(String[] args) {
final String filename = args[0];
final int k = Integer.parseInt(args[1]);
SwingUtilities.invokeLater(
() -> new AutocompleteGUI(filename, k).setVisible(true));
}
}