/* ***************************************************************************** * 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 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 = "Harry Potter and the Deathly Hallows: Part 1 (2010)"; /** * 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(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("", ""); 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] = "" + "
" + query.substring(0, textLen + 1) + "" + query.substring(textLen + 1) + ""; if (displayWeights) { String weight = next.substring(0, tab).trim(); results[i] += "" + "" + weight + ""; } results[i] += "
"; } 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("", ""); 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)); } }