diff --git a/src/simplejavatexteditor/AutoComplete.java b/src/simplejavatexteditor/AutoComplete.java new file mode 100644 index 0000000..0f9c88b --- /dev/null +++ b/src/simplejavatexteditor/AutoComplete.java @@ -0,0 +1,283 @@ +package simplejavatexteditor; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.ArrayList; +import java.util.Collections; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.JTextArea; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; + +/** + *

Auto complete functionality multiple programming languages, including brackets and + * parentheses

+ * + *

+ * An ArrayList is created for the keywords and the brackets. + * Logic for setting the content of the ArrayList is + * found in UI.java. If the word currently being typed + * matches a word in the list, a Runnable inner class is + * implemented to handle the word completion. + * + * Two other inner classes are also used. The second one handles when the enter + * key is pressed in response to an auto complete suggestion. The third one + * performs additional logic on brackets. + *

+ * + * + * @author Patrick Slagle + * @since 2016-12-03 + */ +public class AutoComplete + implements DocumentListener { + + private ArrayList brackets = new ArrayList<>(); + private ArrayList bracketCompletions = new ArrayList<>(); + + private ArrayList words = new ArrayList<>(); + + SupportedKeywords kw; + + //Keep track of when code completion + //has been activated + private enum Mode { + + INSERT, COMPLETION + }; + + private final UI ui; + private Mode mode = Mode.INSERT; + private final JTextArea textArea; + private static final String COMMIT_ACTION = "commit"; + private boolean isKeyword; + private int pos; + private String content; + + public AutoComplete(UI ui, ArrayList al) { + //Set the keywords + words = al; + kw = new SupportedKeywords(); + brackets = kw.getbrackets(); + bracketCompletions = kw.getbracketCompletions(); + + //Access the editor + this.ui = ui; + textArea = ui.getEditor(); + + //Set the handler for the enter key + InputMap im = textArea.getInputMap(); + ActionMap am = textArea.getActionMap(); + im.put(KeyStroke.getKeyStroke("ENTER "), COMMIT_ACTION); + am.put(COMMIT_ACTION, new CommitAction()); + + Collections.sort(words); + } + + /** + * A character has been typed into the document. + * This method performs the primary + * check to find a keyword completion. + * + * @param e + */ + @Override + public void insertUpdate(DocumentEvent e) { + pos = e.getOffset(); + content = null; + + try { + content = textArea.getText(0, pos + 1); + } catch (BadLocationException ex) { + ex.printStackTrace(); + } + + if (e.getLength() != 1) { + return; + } + + //Before checking for a keyword + checkForBracket(); + + //Get the beginning of the word being typed + int start; + for (start = pos; start >= 0; start--) { + if (!Character.isLetter(content.charAt(start))) { + break; + } + } + + //Auto complete will start + //after two characters are typed + if (pos - start < 2) { + return; + } + + //Search for a match on the word being typed + //in the keywords ArrayList + String prefix = content.substring(start + 1); + int n = Collections.binarySearch(words, prefix); + + if (n < 0 && -n < words.size()) { + String match = words.get(-n - 1); + + if (match.startsWith(prefix)) { + String completion = match.substring(pos - start); + isKeyword = true; + SwingUtilities.invokeLater( + new CompletionTask(completion, pos + 1)); + } else { + mode = Mode.INSERT; + } + } + } + + /** + * Performs a check to see if the last + * key typed was one of the supported + * bracket characters + */ + private void checkForBracket() { + //String of the last typed character + char c = content.charAt(pos); + String s = String.valueOf(c); + + for (int i = 0; i < brackets.size(); i++) { + if (brackets.get(i).equals(s)) { + isKeyword = false; + SwingUtilities.invokeLater( + new CompletionTask(bracketCompletions.get(i), pos + 1)); + } + } + } + + /** + * So that future classes can view the keyword list in the future. + * + * @return the keywords + */ + private ArrayList getKeywords() { + return words; + } + + /** + * So that these keywords can be modified or added to in the future. + * + * @param keyword the keyword to set + */ + private void setKeywords(String keyword) { + words.add(keyword); + } + + /** + * Handles the auto complete suggestion + * generated when the user is typing a + * word that matches a keyword. + */ + private class CompletionTask + implements Runnable { + + private final String completion; + private final int position; + + public CompletionTask(String completion, int position) { + this.completion = completion; + this.position = position; + } + + @Override + public void run() { + textArea.insert(completion, position); + + textArea.setCaretPosition(position + completion.length()); + textArea.moveCaretPosition(position); + mode = Mode.COMPLETION; + if (!isKeyword) { + textArea.addKeyListener(new HandleBracketEvent()); + } + } + } + + /** + * Enter key is pressed in response to an + * auto complete suggestion. Respond + * appropriately. + */ + private class CommitAction + extends AbstractAction { + + @Override + public void actionPerformed(ActionEvent e) { + + if (mode == Mode.COMPLETION) { + int pos = textArea.getSelectionEnd(); + + if (isKeyword) { + textArea.insert(" ", pos); + textArea.setCaretPosition(pos + 1); + mode = Mode.INSERT; + } + } else { + mode = Mode.INSERT; + textArea.replaceSelection("\n"); + } + } + } + + /** + * Additional logic for bracket auto complete + */ + private class HandleBracketEvent + implements KeyListener { + + @Override + public void keyTyped(KeyEvent e) { + //Bracket auto complete needs special attention. + //Multiple possible responses are needed. + String keyEvent = String.valueOf(e.getKeyChar()); + for (String bracketCompletion : bracketCompletions) { + if (keyEvent.equals(bracketCompletion)) { + textArea.replaceRange("", pos, pos + 1); + mode = Mode.INSERT; + textArea.removeKeyListener(this); + } + } + int currentPosition = textArea.getCaretPosition(); + switch (e.getKeyChar()) { + case '\n': + textArea.insert("\n\n", currentPosition); + textArea.setCaretPosition(currentPosition + 1); + mode = Mode.INSERT; + textArea.removeKeyListener(this); + break; + default: + textArea.setCaretPosition(pos); + mode = Mode.INSERT; + textArea.removeKeyListener(this); + break; + } + } + + @Override + public void keyPressed(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + } + } + + @Override + public void removeUpdate(DocumentEvent e) { + } + + @Override + public void changedUpdate(DocumentEvent e) { + } +} \ No newline at end of file diff --git a/src/simplejavatexteditor/SupportedKeywords.java b/src/simplejavatexteditor/SupportedKeywords.java new file mode 100644 index 0000000..ef4e590 --- /dev/null +++ b/src/simplejavatexteditor/SupportedKeywords.java @@ -0,0 +1,62 @@ +package simplejavatexteditor; + +import java.util.ArrayList; + +/** + *

A class to store the programming language keywords and + * provide access to them.

+ * + *

Makes multiple language support possible and makes adding new language + * support convenient. To add more keywords, add a string array and getters + * to this class. Then, update the switch statement in UI.java.

+ */ +public class SupportedKeywords { + private String[] java = {"abstract", "assert", "boolean", + "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "extends", "false", + "final", "finally", "float", "for", "goto", "if", "implements", + "import", "instanceof", "int", "System", "out", "print()", "println()", + "new", "null", "package", "private", "protected", "public", "interface", + "long", "native", "return", "short", "static", "strictfp", "super", "switch", + "synchronized", "this", "throw", "throws", "transient", "true", + "try", "void", "volatile", "while", "String"}; + + private String[] cpp = { "auto", "const", "double", "float", "int", "short", + "struct", "unsigned", "break", "continue", "else", "for", "long", "signed", + "switch", "void", "case", "default", "enum", "goto", "register", "sizeof", + "typedef", "volatile", "char", "do", "extern", "if", "return", "static", + "union", "while", "asm", "dynamic_cast", "namespace", "reinterpret_cast", "try", + "bool", "explicit", "new", "static_cast", "typeid", "catch", "false", "operator", + "template", "typename", "class", "friend", "private", "this", "using", "const_cast", + "inline", "public", "throw", "virtual", "delete", "mutable", "protected", "true", "wchar_t" }; + + private String[] brackets = { "{", "(" }; + private String[] bCompletions = { "}", ")" }; + public String[] getJavaKeywords() { + return java; + } + public String[] getCppKeywords() { + return cpp; + } + public ArrayList getbracketCompletions() { + ArrayList al = new ArrayList<>(); + for(String completion : bCompletions) { + al.add(completion); + } + return al; + } + public ArrayList getbrackets() { + ArrayList al = new ArrayList<>(); + for(String completion : brackets) { + al.add(completion); + } + return al; + } + public ArrayList setKeywords(String[] arr) { + ArrayList al = new ArrayList<>(); + for(String words : arr) { + al.add(words); + } + return al; + } +} \ No newline at end of file diff --git a/src/simplejavatexteditor/UI.java b/src/simplejavatexteditor/UI.java index 053932e..8e6d343 100644 --- a/src/simplejavatexteditor/UI.java +++ b/src/simplejavatexteditor/UI.java @@ -19,19 +19,26 @@ package simplejavatexteditor; // GUI + import javax.swing.*; -import java.awt.BorderLayout; -import java.awt.Container; -import java.awt.Font; -import java.awt.event.*; +import javax.swing.border.Border; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.Scanner; + // Input Stream -import java.io.*; // Various -import java.util.Scanner; -import javax.swing.border.Border; public class UI extends JFrame implements ActionListener { - + private static final long serialVersionUID = 1L; private final Container container; private final JTextArea textArea; @@ -39,40 +46,50 @@ public class UI extends JFrame implements ActionListener { private final JMenu menuFile, menuEdit, menuFind, menuAbout; private final JMenuItem newFile, openFile, saveFile, close, clearFile, quickFind, aboutMe, aboutSoftware; private final JToolBar mainToolbar; - JButton newButton, openButton, saveButton, clearButton, quickButton, aboutMeButton, aboutButton, closeButton, spaceButton1, spaceButton2; - + JButton newButton, openButton, + saveButton, clearButton, + quickButton, aboutMeButton, + aboutButton, closeButton, + spaceButton1, spaceButton2; + //setup icons - File Menu private final ImageIcon newIcon = new ImageIcon("icons/new.png"); private final ImageIcon openIcon = new ImageIcon("icons/open.png"); private final ImageIcon saveIcon = new ImageIcon("icons/save.png"); private final ImageIcon closeIcon = new ImageIcon("icons/close.png"); - + //setup icons - Search Menu private final ImageIcon clearIcon = new ImageIcon("icons/clear.png"); - + //setup icons - Search Menu private final ImageIcon searchIcon = new ImageIcon("icons/search.png"); - + //setup icons - Help Menu private final ImageIcon aboutMeIcon = new ImageIcon("icons/about_me.png"); private final ImageIcon aboutIcon = new ImageIcon("icons/about.png"); - - public UI() { + + AutoComplete autocomplete; + private boolean hasListener = false; + + public UI() { container = getContentPane(); - + // Set the initial size of the window - setSize(700, 500); - + setSize(700, 500); + // Set the title of the window setTitle("Undefined | " + SimpleJavaTextEditor.NAME); - + // Set the default close operation (exit when it gets closed) - setDefaultCloseOperation(EXIT_ON_CLOSE); - + setDefaultCloseOperation(EXIT_ON_CLOSE); + // Set a default font for the TextArea textArea = new JTextArea("", 0,0); - textArea.setFont(new Font("Century Gothic", Font.BOLD, 12)); - + textArea.setFont(new Font("Century Gothic", Font.BOLD, 12)); + textArea.setTabSize(2); + textArea.setFont(new Font("Century Gothic", Font.BOLD, 12)); + textArea.setTabSize(2); + // This is why we didn't have to worry about the size of the TextArea! getContentPane().setLayout(new BorderLayout()); // the BorderLayout bit makes it fill it automatically getContentPane().add(textArea); @@ -82,7 +99,7 @@ public UI() { menuEdit = new JMenu("Edit"); menuFind = new JMenu("Search"); menuAbout = new JMenu("About"); - + // Set the Items Menu newFile = new JMenuItem("New", newIcon); openFile = new JMenuItem("Open", openIcon); @@ -92,31 +109,31 @@ public UI() { quickFind = new JMenuItem("Quick", searchIcon); aboutMe = new JMenuItem("About Me", aboutMeIcon); aboutSoftware = new JMenuItem("About Software", aboutIcon); - + // Set the Menu Bar into the our GUI menuBar = new JMenuBar(); - menuBar.add(menuFile); + menuBar.add(menuFile); menuBar.add(menuEdit); menuBar.add(menuFind); - menuBar.add(menuAbout); + menuBar.add(menuAbout); this.setJMenuBar(menuBar); - + // New File newFile.addActionListener(this); // Adding an action listener (so we know when it's been clicked). newFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_MASK)); // Set a keyboard shortcut menuFile.add(newFile); // Adding the file menu - + // Open File openFile.addActionListener(this); - openFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_MASK)); - menuFile.add(openFile); - + openFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_MASK)); + menuFile.add(openFile); + // Save File saveFile.addActionListener(this); saveFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_MASK)); menuFile.add(saveFile); - + // Close File /* @@ -132,90 +149,95 @@ public UI() { // Clear File (Code) clearFile.addActionListener(this); - clearFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_MASK)); + clearFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, InputEvent.CTRL_MASK)); menuEdit.add(clearFile); - + // Find Word quickFind.addActionListener(this); quickFind.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_MASK)); menuFind.add(quickFind); - + // About Me aboutMe.addActionListener(this); aboutMe.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0)); menuAbout.add(aboutMe); - + // About Software aboutSoftware.addActionListener(this); aboutSoftware.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F2, 0)); menuAbout.add(aboutSoftware); - + mainToolbar = new JToolBar(); this.add(mainToolbar, BorderLayout.NORTH); //used to create space between button groups Border emptyBorder = BorderFactory.createEmptyBorder(0, 0, 0, 50); - + newButton = new JButton(newIcon); newButton.setToolTipText("New"); newButton.addActionListener(this); mainToolbar.add(newButton); mainToolbar.addSeparator(); - + openButton = new JButton(openIcon); openButton.setToolTipText("Open"); openButton.addActionListener(this); mainToolbar.add(openButton); mainToolbar.addSeparator(); - + saveButton = new JButton(saveIcon); saveButton.setToolTipText("Save"); saveButton.addActionListener(this); mainToolbar.add(saveButton); mainToolbar.addSeparator(); - + clearButton = new JButton(clearIcon); clearButton.setToolTipText("Clear All"); clearButton.addActionListener(this); mainToolbar.add(clearButton); mainToolbar.addSeparator(); - + quickButton = new JButton(searchIcon); quickButton.setToolTipText("Quick Search"); quickButton.addActionListener(this); mainToolbar.add(quickButton); - + //create space between button groups spaceButton1 = new JButton(); spaceButton1.setBorder(emptyBorder); mainToolbar.add(spaceButton1); - + aboutMeButton = new JButton(aboutMeIcon); aboutMeButton.setToolTipText("About Me"); aboutMeButton.addActionListener(this); mainToolbar.add(aboutMeButton); mainToolbar.addSeparator(); - + aboutButton = new JButton(aboutIcon); aboutButton.setToolTipText("About NotePad PH"); aboutButton.addActionListener(this); mainToolbar.add(aboutButton); - + //create space between button groups spaceButton2 = new JButton(); spaceButton2.setBorder(emptyBorder); mainToolbar.add(spaceButton2); - + closeButton = new JButton(closeIcon); closeButton.setToolTipText("Close"); closeButton.addActionListener(this); mainToolbar.add(closeButton); } - + + //make the TextArea available to the autocomplete handler + protected JTextArea getEditor() { + return textArea; + } + public void actionPerformed (ActionEvent e) { // If the source of the event was our "close" option if(e.getSource() == close || e.getSource() == closeButton) this.dispose(); // dispose all resources and close the application - + // If the source was the "new" file option else if(e.getSource() == newFile || e.getSource() == newButton) { FEdit.clear(textArea); @@ -246,7 +268,7 @@ else if(e.getSource() == openFile || e.getSource() == openButton) { // If the source of the event was the "save" option else if(e.getSource() == saveFile || e.getSource() == saveButton) { // Open a file chooser - JFileChooser fileChoose = new JFileChooser(); + JFileChooser fileChoose = new JFileChooser(); // Open the file, only this time we call int option = fileChoose.showSaveDialog(this); @@ -262,16 +284,55 @@ else if(e.getSource() == saveFile || e.getSource() == saveButton) { // Create a buffered writer to write to a file BufferedWriter out = new BufferedWriter(new FileWriter(file.getPath())); // Write the contents of the TextArea to the file - out.write(textArea.getText()); + out.write(textArea.getText()); // Close the file stream - out.close(); + out.close(); + + //If the user saves files with supported + //file types more than once, we need to remove + //previous listeners to avoid bugs. + if(hasListener) { + textArea.getDocument().removeDocumentListener(autocomplete); + hasListener = false; + } + + //With the keywords located in a separate class, + //we can support multiple languages and not have to do + //much to add new ones. + SupportedKeywords kw = new SupportedKeywords(); + ArrayList arrayList; + String[] list = { ".java", ".cpp" }; + + //Iterate through the list, find the supported + //file extension, apply the appropriate getter method from + //the keyword class + for(int i = 0; i < list.length; i++) { + if(file.getName().endsWith(list[i])) { + switch(i) { + case 0: + String[] jk = kw.getJavaKeywords(); + arrayList = kw.setKeywords(jk); + autocomplete = new AutoComplete(this, arrayList); + textArea.getDocument().addDocumentListener(autocomplete); + hasListener = true; + break; + case 1: + String[] ck = kw.getCppKeywords(); + arrayList = kw.setKeywords(ck); + autocomplete = new AutoComplete(this, arrayList); + textArea.getDocument().addDocumentListener(autocomplete); + hasListener = true; + break; + } + } + } } catch (Exception ex) { // again, catch any exceptions and... // ...write to the debug console System.out.println(ex.getMessage()); } } } - + // Clear File (Code) if(e.getSource() == clearFile || e.getSource() == clearButton) { FEdit.clear(textArea); @@ -279,8 +340,8 @@ else if(e.getSource() == saveFile || e.getSource() == saveButton) { // Find if(e.getSource() == quickFind || e.getSource() == quickButton) { new Find(textArea); - } - + } + // About Me else if(e.getSource() == aboutMe || e.getSource() == aboutMeButton) { new About().me(); @@ -289,7 +350,6 @@ else if(e.getSource() == aboutMe || e.getSource() == aboutMeButton) { else if(e.getSource() == aboutSoftware || e.getSource() == aboutButton) { new About().software(); } - + } - -} \ No newline at end of file +}