I'm new to Java and I've written a flashcard program in Java 8 that allows the user to create a collection of flashcards and then test themselves using those flashcards. Please provide any recommendations on the readability and the design of the program (including my use of OOP).
Here's some specific areas that might be helpful:
Have I used encapsulation properly?
Is my use of inner classes in
QuizCardPlayer
ok?In the
build()
method inQuizCardPlayer
, would it be better to useSwingUtilities.invokeLater()
inside every otherbuild***()
method instead of using it in just thebuild()
method?
Any feedback regarding my JavaDoc style comments are welcome too (I felt that a lot of the functions are self-descriptive enough for there to not be a need for comments).
MainCode
(this class only really exists to create an instance of the flashcard program)
/** MainCode - creates an instance of a simple flashcard program */ public class MainCode { public static void main(String[] args){ MainCode q = new MainCode(); q.go(); } private void go(){ QuizCardBuilder quizCardBuilder = new QuizCardBuilder(new Deck()); quizCardBuilder.build(); } }
QuizCardBuilder
(creates and edits a collection of flashcards)
import java.awt.*; import java.awt.event.*; import javax.swing.*; /** QuizCardBuilder - This class allows the user to create, edit and save a Deck of QuizCards. */ public class QuizCardBuilder { private Deck deck; private JButton button; private JFileChooser fileChooser = new JFileChooser(); private JFrame frame; private JTextArea answerText = new JTextArea(); private JTextArea questionText = new JTextArea(); private JPanel panel; private QuizCardPlayer quizCardPlayer; public QuizCardBuilder(Deck deck) { this.deck = deck; createQuizCardPlayer(); } /** addCard - adds a QuizCard to the current Deck. */ private void addCard(){ deck.addQuizCard(getQuestionText().getText(), getAnswerText().getText()); setQuestionText(null); setAnswerText(null); } void build() { SwingUtilities.invokeLater( () -> { buildFrame(); buildContentPane(); buildMenuBar(); buildLabel(new JLabel("Question:")); buildTextArea(questionText); buildLabel(new JLabel("Answer:")); buildTextArea(answerText); buildButtonPanel(); displayFrame(); questionText.requestFocusInWindow(); } ); } private void buildButtonPanel() { button = new JButton("Add"); button.setAlignmentX(Component.LEFT_ALIGNMENT); button.addActionListener(ev -> addCard()); panel.add(button); } private void buildContentPane() { panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); panel.setBorder(BorderFactory.createEmptyBorder(5, 15, 0, 15)); frame.setContentPane(panel); } private void buildFrame() { frame = new JFrame("Quiz card builder - " + deck.getFileName()); frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); frame.setMinimumSize(new Dimension(400, 400)); frame.addWindowListener( new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { close(); } } ); } private void buildLabel(JLabel label) { label.setAlignmentX(Component.LEFT_ALIGNMENT); label.setFont(FontConstants.labelFont); panel.add(label); } private void buildMenuBar() { JMenuBar jMenuBar = new JMenuBar(); JMenu file = new JMenu("File"); file.add(Open); file.add(Save); file.add(SaveAs); file.add(Exit); JMenu card = new JMenu("Deck"); card.add(ShuffleDeck); card.add(Play); jMenuBar.add(file); jMenuBar.add(card); frame.setJMenuBar(jMenuBar); } private void buildTextArea(JTextArea jTextArea) { jTextArea.setWrapStyleWord(true); jTextArea.setLineWrap(true); jTextArea.setFont(FontConstants.textAreaFont); jTextArea.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { deck.setIsModified(true); } }); JScrollPane jsp = new JScrollPane(jTextArea); jsp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); jsp.setAlignmentX(Component.LEFT_ALIGNMENT); panel.add(jsp); } private void close(){ if (deck.getIsModified()) { // Automatically closes the program if there's nothing to be saved. if(deck.getQuizCardList().size() == 0 && getQuestionText().getText().length() == 0 && getAnswerText().getText().length() == 0) { System.exit(0); }else { int optionChosen = JOptionPane.showConfirmDialog(frame, "Do you want to save this deck?", "Save", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); if (optionChosen == JOptionPane.YES_OPTION) { save(); } if (optionChosen != JOptionPane.CANCEL_OPTION) { System.exit(0); } } }else{ System.exit(0); } } /** createQuizCardPlayer - safely creates an instance of QuizCardPlayer, whilst allowing QuizCardPlayer to * have a callback */ private void createQuizCardPlayer(){ quizCardPlayer = new QuizCardPlayer(deck); quizCardPlayer.registerQuizCardBuilder(this); // registers the callback } private void displayFrame() { frame.pack(); frame.setLocationRelativeTo(null); frame.setVisible(true); } /** openFile - opens a saved Deck */ private void openFile(){ int optionChosen = JOptionPane.YES_OPTION; if(deck.getIsModified()){ optionChosen = JOptionPane.showConfirmDialog(frame, "Do you want to save this deck before " + "opening another?", "Save", JOptionPane.YES_NO_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE); if(optionChosen == JOptionPane.YES_OPTION){ save(); } } if(optionChosen != JOptionPane.CANCEL_OPTION && fileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION){ deck = new Deck(); deck.readFile(fileChooser.getSelectedFile().getAbsolutePath()); setTitle(deck.getFileName()); setQuestionText(null); setAnswerText(null); } } /** save - Saves the current Deck under the same name, if previously saved. If the Deck is new, * then saveAs is invoked */ private void save(){ if(deck.getFileName().equals("Untitled")){ saveAs(); }else{ if(getQuestionText().getText().length() > 0){ addCard(); } deck.save(deck.getFileLocation()); deck.setIsModified(false); } } /** saveAs - User gets to choose the filename that stores the current Deck */ private void saveAs(){ if(fileChooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) { if(getQuestionText().getText().length() > 0){ addCard(); } deck.save(fileChooser.getSelectedFile().getAbsolutePath()); deck.setFileName(fileChooser.getSelectedFile().getName()); setTitle(deck.getFileName()); deck.setIsModified(false); } } // GETTERS private JTextArea getAnswerText() { return answerText; } JTextArea getQuestionText() { return questionText; } // SETTERS private void setAnswerText(String text) { SwingUtilities.invokeLater(() -> answerText.setText(text)); } void setTextAreaEditability(boolean isEditable){ questionText.setEditable(isEditable); answerText.setEditable(isEditable); button.setEnabled(isEditable); } private void setTitle(String newTitle){ SwingUtilities.invokeLater(() -> frame.setTitle("Quiz Card Builder - " + newTitle)); } private void setQuestionText(String text) { SwingUtilities.invokeLater(() -> questionText.setText(text)); } // ACTIONS private Action Exit = new AbstractAction("Quit"){ @Override public void actionPerformed(ActionEvent ev){ close(); } }; private Action Open = new AbstractAction("Open"){ @Override public void actionPerformed(ActionEvent ev){ openFile(); } }; private Action Play = new AbstractAction("Begin test"){ @Override public void actionPerformed(ActionEvent ev){ // Allows the user to open a file if no file is already open if(deck.getQuizCardList().size() == 0) { openFile(); } // Prevents window from popping up if there's no QuizCards to use if(deck.getQuizCardList().size() > 0) { if (deck.getIsTestRunning()) { Toolkit.getDefaultToolkit().beep(); quizCardPlayer.toFront(); } else { deck.setIsTestRunning(true); setTextAreaEditability(false); createQuizCardPlayer(); quizCardPlayer.build(); } } } }; private Action Save = new AbstractAction("Save"){ @Override public void actionPerformed(ActionEvent ev){ save(); } }; private Action SaveAs = new AbstractAction("Save as...") { @Override public void actionPerformed(ActionEvent e) { saveAs(); } }; private Action ShuffleDeck = new AbstractAction("Shuffle deck"){ @Override public void actionPerformed(ActionEvent ev){ deck.shuffle(); } }; }
QuizCardPlayer
(this class allows the user to test themselves)
import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import javax.swing.*; /** QuizCardPlayer - This class allows the user to test themselves using a specific Deck of QuizCards. */ public class QuizCardPlayer { private static final Dimension FRAME_SIZE = new Dimension(300, 300); private static final Dimension MINIMUM_FRAME_SIZE = new Dimension(200, 200); private int deckIndex; private boolean isAnswerShown; private Deck deck; private JButton correctButton, showAnswerButton, wrongButton; private JFrame frame; private JLabel label; private JPanel contentPane; private JTextArea textArea; private QuizCardBuilder quizCardBuilder; public QuizCardPlayer(Deck deck){ this.deck = deck; } void build(){ SwingUtilities.invokeLater( () -> { buildFrame(); buildContentPane(); buildLabel(); buildTextArea(); buildButtonPanel(); displayFrame(); showAnswerButton.requestFocusInWindow(); } ); } private void buildButtonPanel(){ showAnswerButton = new JButton("Show answer"); showAnswerButton.addActionListener(new ButtonListener()); correctButton = new JButton("Right"); correctButton.addActionListener(new CorrectButtonListener()); correctButton.setVisible(false); wrongButton = new JButton("Wrong"); wrongButton.addActionListener(new WrongButtonListener()); wrongButton.setVisible(false); JPanel buttonPanel = new JPanel(); buttonPanel.add(showAnswerButton); buttonPanel.add(correctButton); buttonPanel.add(wrongButton); buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); contentPane.add(BorderLayout.SOUTH, buttonPanel); } private void buildContentPane(){ contentPane = new JPanel(); contentPane.setLayout(new BorderLayout()); contentPane.setBorder(BorderFactory.createEmptyBorder(5, 15, 0, 15)); frame.setContentPane(contentPane); } private void buildFrame(){ frame = new JFrame("Quiz card Player - " + deck.getFileName()); frame.setMinimumSize(MINIMUM_FRAME_SIZE); frame.addWindowListener( new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { closeFrame(); } } ); } private void buildLabel(){ label = new JLabel("Question:"); label.setFont(FontConstants.labelFont); label.setAlignmentX(Component.LEFT_ALIGNMENT); contentPane.add(BorderLayout.NORTH, label); } private void buildTextArea(){ textArea = new JTextArea(); textArea.setEditable(false); textArea.setLineWrap(true); textArea.setWrapStyleWord(true); textArea.setText(deck.getQuizCardList().get(0).getQuestion()); textArea.setFont(FontConstants.textAreaFont); JScrollPane jsp = new JScrollPane(textArea); jsp.setAlignmentX(Component.LEFT_ALIGNMENT); contentPane.add(BorderLayout.CENTER, jsp); } private void closeFrame(){ SwingUtilities.invokeLater(frame::dispose); deck.setIsTestRunning(false); deck.setNumCorrect(0); deck.setNumWrong(0); quizCardBuilder.setTextAreaEditability(true); quizCardBuilder.getQuestionText().requestFocusInWindow(); } private void displayFrame(){ frame.setSize(FRAME_SIZE); frame.setLocationRelativeTo(null); frame.setVisible(true); } /** registerQuizCardBuilder - A callback function that allows an instance of QuizCardPlayer to pass info back to * the specified instance of QuizCardBuilder. */ void registerQuizCardBuilder(QuizCardBuilder newQuizCardBuilder){ quizCardBuilder = newQuizCardBuilder; } /** toFront - brings this frame in the JVM to the front. */ void toFront(){ SwingUtilities.invokeLater(frame::toFront); } // LISTENERS private class CorrectButtonListener extends ButtonListener { @Override public void actionPerformed(ActionEvent ev){ deck.setNumCorrect(deck.getNumCorrect() + 1); super.actionPerformed(ev); } } private class WrongButtonListener extends ButtonListener { // TODO why is it possible to have a public method in a private class? @Override public void actionPerformed(ActionEvent ev){ deck.setNumWrong(deck.getNumWrong() + 1); super.actionPerformed(ev); } } private class ButtonListener implements ActionListener { @Override public void actionPerformed(ActionEvent ev){ if(deckIndex < deck.getQuizCardList().size()) { if (isAnswerShown) { showNextCard(); } else { showAnswer(); } }else if(deckIndex == deck.getQuizCardList().size()) { showResults(); }else{ closeFrame(); } } private void showAnswer(){ SwingUtilities.invokeLater( () -> { label.setText("Answer:"); textArea.setText(deck.getQuizCardList().get(deckIndex).getAnswer()); isAnswerShown = true; showAnswerButton.setVisible(false); correctButton.setVisible(true); correctButton.requestFocusInWindow(); wrongButton.setVisible(true); deckIndex++; } ); } private void showNextCard(){ SwingUtilities.invokeLater( () -> { label.setText("Question:"); textArea.setText(deck.getQuizCardList().get(deckIndex).getQuestion()); isAnswerShown = false; showAnswerButton.setText("Show answer"); showAnswerButton.setVisible(true); showAnswerButton.requestFocusInWindow(); correctButton.setVisible(false); wrongButton.setVisible(false); } ); } private void showResults(){ SwingUtilities.invokeLater( () -> { label.setText("Results:"); textArea.setText("You got " + deck.getNumCorrect() + " correct and " + deck.getNumWrong() + " wrong."); showAnswerButton.setText("End"); showAnswerButton.setVisible(true); showAnswerButton.requestFocusInWindow(); correctButton.setVisible(false); wrongButton.setVisible(false); deckIndex++; } ); } } }
Deck
(manages the collection of flashcards)
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.FileReader; import java.io.FileWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** Deck - This class deals with a collection of QuizCards */ public class Deck { private File file; private List<QuizCard> quizCardList = new ArrayList<>(); private String fileName = "Untitled"; private boolean isModified; private boolean isTestRunning; private int numCorrect; private int numWrong; private static final String QUIZ_CARD_TERMINATOR = "\n29rje2r9\n"; private static final String QUIZ_CARD_SEPARATOR = "\te23bf0hj\t"; /** addQuizCard - creates and adds a QuizCard to quizCardList */ void addQuizCard(String q, String a){ // Prevents any parsing exceptions occurring when opening a file if(q.length() == 0){ q = " "; } if(a.length() == 0){ a = " "; } quizCardList.add(new QuizCard(q, a)); } /** parseData - parses the data from an input String using specified terminators and separators. */ private void parseData(String unparsedData) { String[] stageOne = unparsedData.split(QUIZ_CARD_TERMINATOR); for (String stageTwo : stageOne) { String[] quizCardData = stageTwo.split(QUIZ_CARD_SEPARATOR); addQuizCard(quizCardData[0], quizCardData[1]); } } /** readFile - loads in the data from a saved deck into quizCardList */ void readFile(String fileLocation){ file = new File(fileLocation); setFileName(file.getName()); assert file.canRead(); try(BufferedReader input = new BufferedReader(new FileReader(file))){ int letterNumber; StringBuilder dataToParse = new StringBuilder(); while((letterNumber = input.read()) != -1){ dataToParse.append((char) letterNumber); } parseData(dataToParse.toString()); }catch(IOException ioEx){ ioEx.printStackTrace(); } } /** save - saves the Deck to specified file location */ void save(String fileLocation){ file = new File(fileLocation); assert file.canWrite(); try (BufferedWriter output = new BufferedWriter(new FileWriter(file))) { for(QuizCard quizCard : quizCardList){ output.write(quizCard.getQuestion() + QUIZ_CARD_SEPARATOR + quizCard.getAnswer() + QUIZ_CARD_TERMINATOR); } }catch(IOException ioEx){ ioEx.printStackTrace(); } } /** shuffle - Shuffles the deck in place. If saved, the quiz cards will be saved in the new shuffled order. */ void shuffle(){ Collections.shuffle(quizCardList); } // GETTERS String getFileLocation(){ return file.getAbsolutePath(); } String getFileName(){ return fileName; } boolean getIsModified(){ return isModified; } boolean getIsTestRunning(){ return isTestRunning; } int getNumCorrect(){ return numCorrect; } int getNumWrong(){ return numWrong; } List<QuizCard> getQuizCardList(){ return quizCardList; } // SETTERS void setFileName(String fileName) { if(fileName.contains(".")){ fileName = fileName.split("\\.")[0]; } this.fileName = fileName; } void setIsModified(boolean newValue){ isModified = newValue; } void setIsTestRunning(boolean newValue){ isTestRunning = newValue; } void setNumCorrect(int newValue){ numCorrect = newValue; } void setNumWrong(int newValue){ numWrong = newValue; } }
QuizCard
(the class for one flashcard)
/** QuizCard - Class for one flash card */ public class QuizCard { private String question; private String answer; public QuizCard(String f, String b){ setQuestion(f); setAnswer(b); } // GETTERS String getAnswer(){ return answer; } String getQuestion(){ return question; } // SETTERS void setAnswer(String text){ answer = text; } void setQuestion(String text){ question = text; } }
FontConstants
(holds the fonts used throughout the program)
import java.awt.Font; import javax.swing.UIManager; /** FontConstants - Class used to set the fonts */ public class FontConstants { public static final Font labelFont = new Font(UIManager.getDefaults().getFont("TabbedPane.font").getFamily(), Font.PLAIN, 14); public static final Font textAreaFont = new Font(UIManager.getDefaults().getFont("TabbedPane.font").getFamily(), Font.PLAIN, 16); }