diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index af7a53a74ac..0cb9e834301 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 0, 1, false); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/logic/search/AutoComplete.java b/src/main/java/seedu/address/logic/search/AutoComplete.java new file mode 100644 index 00000000000..57546e9ba71 --- /dev/null +++ b/src/main/java/seedu/address/logic/search/AutoComplete.java @@ -0,0 +1,112 @@ +package seedu.address.logic.search; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.MainApp; +import seedu.address.model.autocomplete.AutoCompleteModel; +import seedu.address.model.autocomplete.Word; + +/** + * Main controller class to execute the searching logic + */ +public class AutoComplete { + + private static final int numOfSug = 10; + + private static AutoCompleteModel acModel; + private static Word[] matchedResults; + private static boolean displayWeights; + + public AutoComplete() { + matchedResults = new Word[numOfSug]; + displayWeights = false; + } + + /** + * Read word storage from txt + * + * @param is path for the library of words + * @return the suggestions to be shown in textField + */ + private static Word[] readWordsFromFile(InputStream is) { + Word[] queries = null; + try { + is.mark(0); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + int lines = 0; + while (reader.readLine() != null) { + lines++; + } + is.reset(); + queries = new Word[lines]; + for (int i = 0; i < lines; i++) { + String line = reader.readLine(); + if (line == null) { + System.err.println("Could not read line " + (i + 1)); + System.exit(1); + } + int tab = line.indexOf('\t'); + if (tab == -1) { + System.err.println("No tab character in line " + (i + 1)); + System.exit(1); + } + long weight = Long.parseLong(line.substring(0, tab).trim()); + String query = line.substring(tab + 1); + queries[i] = new Word(query, weight); + } + reader.close(); + } catch (Exception e) { + System.err.println("Could not read or parse input file "); + e.printStackTrace(); + System.exit(1); + } + + return queries; + } + + /** + * Initialize an autocomplete model + */ + public static void initAc() { + InputStream in = MainApp.class.getResourceAsStream("/data/vocabulary.txt"); + Word[] data = readWordsFromFile(in); + acModel = new AutoCompleteModel(data); + } + + /** + * Makes a call to the implementation of AutoCompleteModel to get + * suggestions for the currently entered text. + * + * @param text string to search for + */ + public static List getSuggestions(String text) { + + List suggestions = new ArrayList<>(); + // don't search for suggestions if there is no input + if (text.length() > 0) { + // get all matching words + Word[] allResults = acModel.allMatches(text); + if (allResults == null) { + throw new NullPointerException("allMatches() is null"); + } + + if (allResults.length > numOfSug) { + matchedResults = Arrays.copyOfRange(allResults, 0, numOfSug); + } else { + matchedResults = allResults; + } + + if (!displayWeights) { + for (Word matchedResult : matchedResults) { + suggestions.add(matchedResult.getQuery()); + } + } + } + return suggestions; + } +} diff --git a/src/main/java/seedu/address/logic/search/BinarySearch.java b/src/main/java/seedu/address/logic/search/BinarySearch.java new file mode 100644 index 00000000000..5ab63beade0 --- /dev/null +++ b/src/main/java/seedu/address/logic/search/BinarySearch.java @@ -0,0 +1,99 @@ +package seedu.address.logic.search; + +import java.util.Comparator; + +/** + * Utility class to find the first and the last index of key using Binary Search. + */ +public class BinarySearch { + + /** + * Searches the specified array for the specified value using modification of binary + * search algorithm and returns the index of the first key in list[] that equals the search key, + * or -1 if no such key were found. + * + * @param list - the array of keys to be searched + * @param key - the value to be searched for + * @param comparator - the comparator by which array is ordered + * @return - the index of the last key in list that equals the search key, -1 if not found + * @throws NullPointerException - if list is null + * @throws NullPointerException - if key is null + * @throws NullPointerException - if comparator is null + */ + public static int firstIndexOf(T[] list, T key, Comparator comparator) { + if (list == null || key == null || comparator == null) { + throw new IllegalArgumentException(); + } + + if (list.length == 0) { + return -1; + } + if (comparator.compare(list[0], key) == 0) { + return 0; + } + + int lo = 0; + int hi = list.length - 1; + + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int cmp = comparator.compare(list[mid], key); + + if (cmp >= 1) { + hi = mid - 1; + } else if (cmp <= -1) { + lo = mid + 1; + } else if (comparator.compare(list[mid - 1], list[mid]) == 0) { + hi = mid - 1; + } else { + return mid; + } + } + return -1; + } + + /** + * Searches the specified array for the specified value using modification of binary + * search algorithm and returns the index of the last key in list[] that equals the search key, + * or -1 if no such key were found. + * + * @param list - the array of keys to be searched + * @param key - the value to be searched for + * @param comparator - the comparator by which array is ordered + * @return - the index of the last key in list that equals the search key, -1 if not found + * @throws NullPointerException - if list is null + * @throws NullPointerException - if key is null + * @throws NullPointerException - if comparator is null + */ + public static int lastIndexOf(T[] list, T key, Comparator comparator) { + if (list == null || key == null || comparator == null) { + throw new IllegalArgumentException(); + } + + if (list.length == 0) { + return -1; + } + if (comparator.compare(list[list.length - 1], key) == 0) { + return list.length - 1; + } + + int lo = 0; + int hi = list.length - 1; + + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + int cmp = comparator.compare(list[mid], key); + + if (cmp >= 1) { + hi = mid - 1; + } else if (cmp <= -1) { + lo = mid + 1; + } else if (comparator.compare(list[mid + 1], list[mid]) == 0) { + lo = mid + 1; + } else { + return mid; + } + } + return -1; + } +} diff --git a/src/main/java/seedu/address/model/autocomplete/AutoCompleteModel.java b/src/main/java/seedu/address/model/autocomplete/AutoCompleteModel.java new file mode 100644 index 00000000000..74faa2a2fa3 --- /dev/null +++ b/src/main/java/seedu/address/model/autocomplete/AutoCompleteModel.java @@ -0,0 +1,59 @@ +package seedu.address.model.autocomplete; + +import java.util.Arrays; + +import seedu.address.logic.search.BinarySearch; + +/** + * Given the data and the query, this class is for searching for words in the data + * starting with the given query and returns the array in descending order w.r.t weight. + */ +public class AutoCompleteModel { + + private final Word[] dataCopy; + + /** + * Initialize an AutoCompleteModel using array of words. + * + * @param data - the array of queries + * @throws NullPointerException - if queries == null + */ + public AutoCompleteModel(Word[] data) { + if (data == null) { + throw new NullPointerException("Data cannot be null"); + } + dataCopy = Arrays.copyOf(data, data.length); + Arrays.sort(dataCopy); + } + + /** + * Return all words that start with the given query, in descending order of weight. + * Query should be non-null. + * + * @param query - query to be searched for + * @return - array of words that match the given query in descending order + * @throws NullPointerException - if query == null + */ + public Word[] allMatches(String query) { + if (query == null) { + throw new NullPointerException("Query cannot be null"); + } + Word queryWord = new Word(query, 100); + int firstIndex = BinarySearch.firstIndexOf(dataCopy, queryWord, Word.compareCharSeq(query.length())); + + // NOT FOUND + if (firstIndex == -1) { + return new Word[0]; + } + int lastIndex = BinarySearch.lastIndexOf(dataCopy, queryWord, Word.compareCharSeq(query.length())); + + int range = lastIndex - firstIndex + 1; + Word[] allMatches = new Word[range]; + int j = 0; + for (int i = firstIndex; i <= lastIndex; i++) { + allMatches[j++] = dataCopy[i]; + } + Arrays.sort(allMatches, Word.compareWeightDescending()); + return allMatches; + } +} diff --git a/src/main/java/seedu/address/model/autocomplete/Word.java b/src/main/java/seedu/address/model/autocomplete/Word.java new file mode 100644 index 00000000000..38c3c5ccbea --- /dev/null +++ b/src/main/java/seedu/address/model/autocomplete/Word.java @@ -0,0 +1,114 @@ +package seedu.address.model.autocomplete; + +import java.util.Comparator; + +/** + * Word is an immutable data type that represents an autocomplete object. + * A query string and an associated integer weight(optional) + * Represents single search word with the query and the weight. + */ +public class Word implements Comparable { + + private final String query; + private final long weight; + + public Word() { + this.query = null; + this.weight = -1; + } + + /** + * Initializes a word with the specified query and the weight. + * Word should be non-null and weight must be non-negative. + * + * @param query - the query to be searched for + * @param weight - the corresponding weight of the query + * @throws NullPointerException - if query == null + * @throws IllegalArgumentException - if weight < 0 + */ + public Word(String query, long weight) { + if (query == null) { + throw new NullPointerException("Word can't be null"); + } + if (weight < 0) { + throw new IllegalArgumentException("Weight must be non-negative"); + } + + this.query = query; + this.weight = weight; + } + + /** + * Returns comparator to compare words in lexicographic order using + * only the first r characters of each query. Parameter r should be non-negative. + * + * @return -1 if first r characters of this are less than the first r characters of that + * 0 if first r characters of this are equal to the first r characters of that + * 1 if first r characters of this are larger than to the first r characters of that + */ + public static Comparator compareCharSeq(int len) { + if (len < 0) { + throw new IllegalArgumentException("length must be non-negative, but was " + len); + } + return (t1, t2) -> { + String q1 = truncateTarget(t1.query, len); + String q2 = truncateTarget(t2.query, len); + return Integer.compare(q1.compareToIgnoreCase(q2), 0); + }; + } + + /** + * For comparing first R chars + * + * @param targetString Desc string to compare + * @param len + * @return + */ + private static String truncateTarget(String targetString, int len) { + final int endIndex = Math.min(targetString.length(), len); + return targetString.substring(0, endIndex); + } + + /** + * Returns comparator for comparing words using their corresponding weights. + */ + public static Comparator compareWeightDescending() { + return Comparator.comparingLong(Word::getWeight).reversed(); + } + + public long getWeight() { + return weight; + } + + public String getQuery() { + return query; + } + + /** + * Returns a string representation of this word in the following format: + * the weight, followed by a tab, followed by the query. + */ + @Override + public String toString() { + return weight + "\t" + query; + } + + /** + * Compares two words in lexicographic order. + * + * @return -1 if this is (less than) that + * 0 if this (is the same as) that + * 1 if this (is larger than) that + */ + @Override + public int compareTo(Word word) { + int cmp = this.query.toLowerCase().compareTo(word.query.toLowerCase()); + if (cmp <= -1) { + return -1; + } else if (cmp >= 1) { + return 1; + } else { + return 0; + } + } +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 7d76e691f52..6175e727d99 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -1,12 +1,23 @@ package seedu.address.ui; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.event.EventHandler; import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.Region; import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.logic.search.AutoComplete; /** * The UI component that is responsible for receiving user command inputs. @@ -21,11 +32,73 @@ public class CommandBox extends UiPart { @FXML private TextField commandTextField; + @FXML + private ListView acSuggestions; + public CommandBox(CommandExecutor commandExecutor) { super(FXML); this.commandExecutor = commandExecutor; // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + autoCompleteListener(); + } + + /** + * Handles the autofill event + */ + private void autoCompleteListener() { + AutoComplete.initAc(); + commandTextField.textProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, + String oldValue, String newValue) { + String[] terms = newValue.split(" "); + String latestQuery = terms[terms.length - 1]; + ObservableList suggestions = + FXCollections.observableArrayList(AutoComplete.getSuggestions(latestQuery)); + acSuggestions.setItems(suggestions); + acSuggestions.setCellFactory(listView -> new AutocompleteListViewCell()); + commandTextField.setOnKeyPressed(new EventHandler() { + @Override + public void handle(KeyEvent event) { + if (event.getCode() == KeyCode.DOWN) { + acSuggestions.requestFocus(); + acSuggestions.getSelectionModel().selectFirst(); + } + } + }); + acSuggestions.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent event) { + setTextField(latestQuery); + acSuggestions.getItems().clear(); + } + }); + acSuggestions.setOnKeyPressed(new EventHandler() { + @Override + public void handle(KeyEvent event) { + if (event.getCode() == KeyCode.ENTER) { + setTextField(latestQuery); + acSuggestions.getItems().clear(); + } + } + }); + Platform.runLater(new Runnable() { + @Override + public void run() { + commandTextField.positionCaret(commandTextField.getLength()); + } + }); + } + }); + } + + private void setTextField(String caretSelection) { + String currentText = commandTextField.getText(); + String sub = currentText.substring(0, currentText.lastIndexOf(caretSelection)); + commandTextField.setText(sub + acSuggestions.getSelectionModel().getSelectedItem()); + commandTextField.requestFocus(); + commandTextField.positionCaret(commandTextField.getLength()); } /** @@ -66,6 +139,7 @@ private void setStyleToIndicateCommandFailure() { */ @FunctionalInterface public interface CommandExecutor { + /** * Executes the command and returns the result. * @@ -74,4 +148,21 @@ public interface CommandExecutor { CommandResult execute(String commandText) throws CommandException, ParseException; } + /** + * Custom {@code ListCell} that displays the graphics of a {@code Query} using a {@code QueryCard}. + */ + class AutocompleteListViewCell extends ListCell { + + @Override + protected void updateItem(String query, boolean empty) { + super.updateItem(query, empty); + + if (empty || query == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new QueryCard(query).getRoot()); + } + } + } } diff --git a/src/main/java/seedu/address/ui/ExpenseCard.java b/src/main/java/seedu/address/ui/ExpenseCard.java index 69245810d94..33aceaf0cac 100644 --- a/src/main/java/seedu/address/ui/ExpenseCard.java +++ b/src/main/java/seedu/address/ui/ExpenseCard.java @@ -14,7 +14,7 @@ */ public class ExpenseCard extends UiPart { - private static final String FXML = "ExpenseListCard.fxml"; + private static final String FXML = "ExpenseCard.fxml"; /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 14945223f9f..530d1db4633 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -75,6 +75,7 @@ private void setAccelerators() { /** * Sets the accelerator of a MenuItem. + * * @param keyCombination the KeyCombination value of the accelerator */ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { @@ -154,7 +155,7 @@ void show() { @FXML private void handleExit() { GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); + (int) primaryStage.getX(), (int) primaryStage.getY()); logic.setGuiSettings(guiSettings); helpWindow.hide(); primaryStage.hide(); diff --git a/src/main/java/seedu/address/ui/QueryCard.java b/src/main/java/seedu/address/ui/QueryCard.java new file mode 100644 index 00000000000..d30cf099868 --- /dev/null +++ b/src/main/java/seedu/address/ui/QueryCard.java @@ -0,0 +1,24 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +/** + * An UI component that displays information of a {@code Query}. + */ +public class QueryCard extends UiPart { + + private static final String FXML = "QueryCard.fxml"; + + @FXML + private HBox apCard; + @FXML + private Label query; + + public QueryCard(String word) { + super(FXML); + query.setText(word); + } +} diff --git a/src/main/resources/data/vocabulary.txt b/src/main/resources/data/vocabulary.txt new file mode 100644 index 00000000000..fc4b64773c9 --- /dev/null +++ b/src/main/resources/data/vocabulary.txt @@ -0,0 +1,47 @@ +1000 add +1000 clear +1000 delete +1000 edit +1000 exit +1000 find +1000 help +1000 list +100 n/ +100 a/ +100 d/ +100 t/ +100 2019 +100 2018 +100 2017 +100 2016 +100 01 +100 02 +100 03 +100 04 +100 05 +100 06 +100 07 +100 08 +100 09 +100 10 +100 11 +100 12 +100 13 +100 14 +100 15 +100 16 +100 17 +100 18 +100 19 +100 20 +100 21 +100 22 +100 23 +100 24 +100 25 +100 26 +100 27 +100 28 +100 29 +100 30 +100 31 diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..c94046165ea 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,15 @@ + + - + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..d3d0e3af077 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -56,11 +56,7 @@ -fx-size: 35; -fx-border-width: 0 0 1 0; -fx-background-color: transparent; - -fx-border-color: - transparent - transparent - derive(-fx-base, 80%) - transparent; + -fx-border-color: transparent transparent derive(-fx-base, 80%) transparent; -fx-border-insets: 0 10 1 0; } @@ -95,7 +91,7 @@ .list-cell { -fx-label-padding: 0 0 0 0; - -fx-graphic-text-gap : 0; + -fx-graphic-text-gap: 0; -fx-padding: 0 0 0 0; } @@ -108,7 +104,7 @@ } .list-cell:filled:selected { - -fx-background-color: #424d5f; + -fx-background-color: #000000; } .list-cell:filled:selected #cardPane { @@ -133,13 +129,13 @@ } .stack-pane { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-color: derive(#1d1d1d, 20%); } .pane-with-border { - -fx-background-color: derive(#1d1d1d, 20%); - -fx-border-color: derive(#1d1d1d, 10%); - -fx-border-top-width: 1px; + -fx-background-color: derive(#1d1d1d, 20%); + -fx-border-color: derive(#1d1d1d, 10%); + -fx-border-top-width: 1px; } .status-bar { @@ -229,8 +225,8 @@ } .button:pressed, .button:default:hover:pressed { - -fx-background-color: white; - -fx-text-fill: #1d1d1d; + -fx-background-color: white; + -fx-text-fill: #1d1d1d; } .button:focused { diff --git a/src/main/resources/view/ExpenseCard.fxml b/src/main/resources/view/ExpenseCard.fxml new file mode 100644 index 00000000000..7f3c1cb1e8d --- /dev/null +++ b/src/main/resources/view/ExpenseCard.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExpenseListCard.fxml b/src/main/resources/view/ExpenseListCard.fxml deleted file mode 100644 index e39e22a2fdd..00000000000 --- a/src/main/resources/view/ExpenseListCard.fxml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 3df886fa07b..f6f54e54983 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -13,48 +13,49 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - - + + + + + + - - - - + + + + diff --git a/src/main/resources/view/QueryCard.fxml b/src/main/resources/view/QueryCard.fxml new file mode 100644 index 00000000000..f31a0ac3d82 --- /dev/null +++ b/src/main/resources/view/QueryCard.fxml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/test b/test deleted file mode 100644 index cb826b50446..00000000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -check test