4
\$\begingroup\$

I made a simple flash card app. The general idea is from Head First Java, but it's one app instead of two (for both making cards and playing them) and the implementation is different. What do you think?

public class App {
 public static void main(String[] args) {
 FlashCardMaker flashCardMaker = new FlashCardMaker();
 flashCardMaker.launch();
 }
}
public record FlashCard(String questionText, String answerText) {
}
import org.example.shared.utilities.CollectionUtil;
import org.example.shared.utilities.UIUtil;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.List;
import java.util.*;
import java.util.stream.Stream;
import static java.awt.BorderLayout.CENTER;
import static java.awt.Font.BOLD;
import static java.awt.Font.PLAIN;
import static javax.swing.BoxLayout.Y_AXIS;
import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER;
import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS;
public class FlashCardMaker {
 //-------------------------------------- COMMON VARIABLES ---------------------------------------
 private AppRegime appRegime = AppRegime.MAKING_CARDS;
 private JPanel mainPanel;
 private JMenuItem newMenuItem;
 private JMenuItem saveMenuItem;
 private JTextArea questionArea;
 private JTextArea answerArea;
 private final Collection<FlashCard> currentCardCollection = new ArrayList<>();
 private final String QUESTION_ANSWER_SEPARATOR = " ===> ";
 //-------------------------------------- GAME VARIABLES ---------------------------------------
 private JMenuItem playLastSetMenuItem;
 private final Collection<FlashCard> lastSavedCardCollection = new HashSet<>();
 private FlashCard currentFlashCardToGuess;
 private Iterator<FlashCard> flashCardIterator;
 public void launch() {
 UIUtil.getUIBuilder()
 .withComponent(CENTER, () -> UIUtil.getComponentBuilder(() -> {
 mainPanel = new JPanel();
 mainPanel.setLayout(new BoxLayout(mainPanel, Y_AXIS));
 return mainPanel;
 })
 .withComponent(() -> {
 var label = new JLabel("Question:");
 label.setHorizontalAlignment(SwingConstants.CENTER);
 return label;
 })
 .withComponent(() -> UIUtil.getComponentBuilder(() -> {
 questionArea = new JTextArea(6, 10);
 questionArea.setLineWrap(true);
 questionArea.setWrapStyleWord(true);
 questionArea.setFont(new Font("sanserif", BOLD, 24));
 return questionArea;
 })
 .setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS)
 .setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER)
 .withBorder(() -> BorderFactory.createEmptyBorder(10, 10, 10, 10))
 .build())
 .withComponent(() -> new JLabel("Answer:"))
 .withComponent(() -> UIUtil.getComponentBuilder(() -> {
 answerArea = new JTextArea(6, 10);
 answerArea.setLineWrap(true);
 answerArea.setWrapStyleWord(true);
 answerArea.setFont(new Font("sanserif", PLAIN, 24));
 return answerArea;
 })
 .setVerticalScrollBarPolicy(VERTICAL_SCROLLBAR_ALWAYS)
 .setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER)
 .withBorder(() -> BorderFactory.createEmptyBorder(10, 10, 10, 10))
 .build())
 .withComponent(() -> {
 var nextButton = new JButton("Next");
 nextButton.addActionListener(new NextButtonListener());
 return nextButton;
 })
 .withBorder(() -> BorderFactory.createEmptyBorder(10, 10, 10, 10))
 .build())
 .withMenuBar(() -> UIUtil.getMenuBarBuilder()
 .withMenu(() -> UIUtil.getMenuBuilder("File")
 .withMenuItem(() -> {
 newMenuItem = new JMenuItem("New");
 newMenuItem.addActionListener(new NewMenuItemListener());
 return newMenuItem;
 })
 .withMenuItem(() -> {
 saveMenuItem = new JMenuItem("Save...");
 saveMenuItem.addActionListener(new SaveMenuItemListener());
 saveMenuItem.setEnabled(false);
 return saveMenuItem;
 })
 .build())
 .withMenu(() -> UIUtil.getMenuBuilder("Play")
 .withMenuItem(() -> {
 playLastSetMenuItem = new JMenuItem("Play Last Saved Set");
 playLastSetMenuItem.addActionListener(new PlayLastSetMenuItemListener());
 playLastSetMenuItem.setEnabled(false);
 return playLastSetMenuItem;
 })
 .withMenuItem(() -> {
 var playSet = new JMenuItem("Play Set...");
 playSet.addActionListener(new PlaySetMenuItemListener());
 return playSet;
 })
 .build())
 .build())
 .withFrameSize(500, 600)
 .visualize();
 }
 private void setUpForNewCardSet() {
 currentCardCollection.clear();
 saveMenuItem.setEnabled(false);
 clearDisplayedCard();
 }
 private void addCurrentCard() {
 FlashCard currentCard = new FlashCard(questionArea.getText(), answerArea.getText());
 currentCardCollection.add(currentCard);
 saveMenuItem.setEnabled(true);
 }
 private void clearDisplayedCard() {
 questionArea.setText("");
 answerArea.setText("");
 questionArea.requestFocus();
 }
 private boolean isCardFilledIn() {
 return !questionArea.getText().isBlank() &&
 !answerArea.getText().isBlank();
 }
 private void saveCards(File file) {
 try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
 for (FlashCard card : currentCardCollection) {
 writer.write(card.questionText() + QUESTION_ANSWER_SEPARATOR);
 writer.write(card.answerText() + "\n");
 }
 } catch (IOException e) {
 throw new RuntimeException(e);
 }
 }
 private void startPlayingCards(Collection<FlashCard> cardSource) {
 clearDisplayedCard();
 Stream.of(questionArea, answerArea).forEach(ta -> ta.setForeground(Color.BLACK));
 Stream.of(newMenuItem, saveMenuItem).forEach(mi -> mi.setEnabled(false));
 flashCardIterator = cardSource.iterator();
 currentFlashCardToGuess = flashCardIterator.next();
 questionArea.setText(currentFlashCardToGuess.questionText());
 answerArea.setText("");
 Stream.of(questionArea, answerArea).forEach(ta -> ta.setEditable(false));
 appRegime = AppRegime.PLAYING;
 }
 private void postGameCleanUp() {
 Stream.of(newMenuItem, saveMenuItem).forEach(mi -> mi.setEnabled(true));
 Stream.of(questionArea, answerArea).forEach(ta -> {
 ta.setEditable(true);
 ta.setForeground(Color.BLACK);
 });
 }
 private class NextButtonListener implements ActionListener {
 @Override
 public void actionPerformed(ActionEvent e) {
 if (appRegime == AppRegime.MAKING_CARDS) {
 if (isCardFilledIn()) {
 addCurrentCard();
 clearDisplayedCard();
 } else {
 UIUtil.requestFocusIfBlank(questionArea, answerArea);
 }
 } else if (appRegime == AppRegime.PLAYING) {
 if (answerArea.getText().isEmpty()) {
 answerArea.setText(currentFlashCardToGuess.answerText());
 } else if (flashCardIterator.hasNext()) {
 currentFlashCardToGuess = flashCardIterator.next();
 questionArea.setText(currentFlashCardToGuess.questionText());
 answerArea.setText("");
 } else {
 Stream.of(questionArea, answerArea).forEach(qa -> qa.setForeground(new Color(131, 9, 143)));
 questionArea.setText("No more cards!");
 answerArea.setText("Click Next to get back to card making");
 appRegime = AppRegime.GAME_COMPLETE;
 }
 } else if (appRegime == AppRegime.GAME_COMPLETE) {
 postGameCleanUp();
 setUpForNewCardSet();
 appRegime = AppRegime.MAKING_CARDS;
 }
 }
 }
 private class NewMenuItemListener implements ActionListener {
 @Override
 public void actionPerformed(ActionEvent e) {
 setUpForNewCardSet();
 }
 }
 private class SaveMenuItemListener implements ActionListener {
 @Override
 public void actionPerformed(ActionEvent e) {
 if (isCardFilledIn()) {
 addCurrentCard();
 clearDisplayedCard();
 }
 if (!currentCardCollection.isEmpty()) {
 JFileChooser fileChooser = new JFileChooser();
 fileChooser.showSaveDialog(mainPanel);
 saveCards(fileChooser.getSelectedFile());
 CollectionUtil.clear(lastSavedCardCollection).andFillFrom(currentCardCollection);
 playLastSetMenuItem.setEnabled(true);
 }
 }
 }
 private class PlayLastSetMenuItemListener implements ActionListener {
 @Override
 public void actionPerformed(ActionEvent e) {
 if (!lastSavedCardCollection.isEmpty()) {
 startPlayingCards(lastSavedCardCollection);
 }
 }
 }
 private class PlaySetMenuItemListener implements ActionListener {
 @Override
 public void actionPerformed(ActionEvent event) {
 JFileChooser fileChooser = new JFileChooser();
 fileChooser.showSaveDialog(mainPanel);
 List<FlashCard> cardsFromFile = new ArrayList<>();
 try (BufferedReader reader = new BufferedReader(new FileReader(fileChooser.getSelectedFile()))) {
 String line;
 while ((line = reader.readLine()) != null) {
 String[] cardTexts = line.split(QUESTION_ANSWER_SEPARATOR);
 FlashCard flashCard = new FlashCard(cardTexts[0], cardTexts[1]);
 cardsFromFile.add(flashCard);
 }
 } catch (IOException e) {
 throw new RuntimeException(e);
 }
 CollectionUtil.clear(currentCardCollection).andFillFrom(cardsFromFile);
 startPlayingCards(currentCardCollection);
 }
 }
 private enum AppRegime {
 MAKING_CARDS, PLAYING, GAME_COMPLETE
 }
}
import java.util.Collection;
public class CollectionUtil {
 public static <T> CollectionModificationBuilder<T> clear(Collection<T> targetCollection) {
 return new CollectionModificationBuilder<>(targetCollection);
 }
 public static class CollectionModificationBuilder<T> {
 private final Collection<T> targetCollection;
 private CollectionModificationBuilder(Collection<T> targetCollection) {
 this.targetCollection = targetCollection;
 }
 public void andFillFrom(Collection<? extends T> sourceCollection) {
 targetCollection.clear();
 targetCollection.addAll(sourceCollection);
 }
 }
}
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.ArrayUtils;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class UIUtil {
 // trimmed
 public static UIBuilder getUIBuilder() {
 return new UIBuilder();
 }
 public static UIBuilder getUIBuilder(Supplier<Container> contentPaneSupplier) {
 return new UIBuilder(contentPaneSupplier);
 }
 public static ComponentBuilder getComponentBuilder(Supplier<JComponent> componentSupplier) {
 return new ComponentBuilder(componentSupplier);
 }
 public static MenuBarBuilder getMenuBarBuilder() {
 return new MenuBarBuilder();
 }
 public static MenuBuilder getMenuBuilder(String menuName) {
 return new MenuBuilder(menuName);
 }
 public static void requestFocusIfBlank(JTextArea textArea, JTextArea... otherTextAreas) {
 Stream.of(ArrayUtils.add(otherTextAreas, textArea))
 .filter(area -> area.getText().isBlank())
 .forEach(JComponent::requestFocus);
 }
 @NoArgsConstructor
 public static class UIBuilder {
 private final JFrame frame;
 private final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
 {
 frame = new JFrame();
 this.withFrameSize(300, 300)
 .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 }
 public UIBuilder(@NotNull Supplier<Container> contentPaneSupplier) {
 Objects.requireNonNull(contentPaneSupplier);
 frame.setContentPane(contentPaneSupplier.get());
 }
 public UIBuilder withComponent(@NotNull String position, @NotNull Supplier<JComponent> componentSupplier) {
 Stream.of(position, componentSupplier).forEach(Objects::requireNonNull);
 getAndAddToPane(position, componentSupplier);
 return this;
 }
 public UIBuilder withBackgroundColor(@NotNull Color color) {
 frame.getContentPane().setBackground(color);
 return this;
 }
 public UIBuilder withFrameSize(int width, int height) {
 checkAgainstScreenSize(width, height);
 int x = (screenSize.width - width) / 2;
 int y = (screenSize.height - height) / 2;
 frame.setBounds(x, y, width, height);
 return this;
 }
 private void getAndAddToPane(@NotNull String position, @NotNull Supplier<JComponent> componentSupplier) {
 JComponent textField = componentSupplier.get();
 frame.getContentPane().add(position, textField);
 }
 private void checkAgainstScreenSize(int width, int height) {
 if (screenSize.width < width) {
 throw new IllegalArgumentException("Frame width cannot be greater than screen width");
 } else if (screenSize.height < height) {
 throw new IllegalArgumentException("Frame height cannot be greater than screen height");
 }
 }
 public UIBuilder withDefaultCloseOperation(int windowConstant) {
 frame.setDefaultCloseOperation(windowConstant);
 return this;
 }
 public void visualize() {
 frame.setVisible(true);
 }
 public UIBuilder withMenuBar(Supplier<JMenuBar> menuBarSupplier) {
 frame.setJMenuBar(menuBarSupplier.get());
 return this;
 }
 }
 public static class ComponentBuilder {
 private JComponent component;
 public ComponentBuilder(@NotNull Supplier<JComponent> panelSupplier) {
 Objects.requireNonNull(panelSupplier);
 this.component = panelSupplier.get();
 }
 public ComponentBuilder withBorder(@NotNull Supplier<Border> borderSupplier) {
 Objects.requireNonNull(borderSupplier);
 component.setBorder(borderSupplier.get());
 return this;
 }
 public ComponentBuilder withComponent(@NotNull String position, @NotNull Supplier<JComponent> componentSupplier) {
 Stream.of(position, componentSupplier).forEach(Objects::requireNonNull);
 component.add(position, componentSupplier.get());
 return this;
 }
 public ComponentBuilder withComponent(@NotNull Supplier<JComponent> componentSupplier) {
 Objects.requireNonNull(componentSupplier);
 component.add(componentSupplier.get());
 return this;
 }
 public ComponentBuilder setVerticalScrollBarPolicy(int verticalScrollbarPolicy) {
 makeComponentScrollableAsNeeded();
 ((JScrollPane) component).setVerticalScrollBarPolicy(verticalScrollbarPolicy);
 return this;
 }
 public ComponentBuilder setHorizontalScrollBarPolicy(int horizontalScrollbarPolicy) {
 makeComponentScrollableAsNeeded();
 ((JScrollPane) component).setHorizontalScrollBarPolicy(horizontalScrollbarPolicy);
 return this;
 }
 private void makeComponentScrollableAsNeeded() {
 if (!(component instanceof JScrollPane)) {
 component = new JScrollPane(component);
 }
 }
 public JComponent build() {
 return component;
 }
 }
 public static class MenuBarBuilder {
 private final JMenuBar menuBar = new JMenuBar();
 public MenuBarBuilder withMenu(Supplier<JMenu> menuSupplier) {
 menuBar.add(menuSupplier.get());
 return this;
 }
 public JMenuBar build() {
 return menuBar;
 }
 }
 public static class MenuBuilder {
 private final JMenu menu;
 public MenuBuilder(String menuName) {
 menu = new JMenu(menuName);
 }
 public MenuBuilder withMenuItem(Supplier<JMenuItem> menuItemSupplier) {
 menu.add(menuItemSupplier.get());
 return this;
 }
 public JMenu build() {
 return menu;
 }
 }
}

enter image description here

For example, this app could benefit from using a custom default extension (but I don't know how to do it with JFileChooser). The layout is a bit cockeyed too (I wish everything were properly centered)

asked Aug 24, 2023 at 23:34
\$\endgroup\$

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.