--- /dev/null
+<!-- Ant build file -->
+<project name="answer_check">
+ <property name="src" location="src" />
+ <property name="out" location="out" />
+
+ <target name="setup">
+ <mkdir dir="${out}/classes" />
+ </target>
+
+ <target name="clean">
+ <delete dir="${out}" />
+ </target>
+
+ <target name="compile" depends="setup">
+ <javac srcdir="${src}" destdir="${out}/classes" debug="true">
+ <compilerarg value="-Xlint:unchecked" />
+ </javac>
+ </target>
+
+ <target name="jar" depends="compile">
+ <jar jarfile="${out}/build.jar" basedir="${out}/classes">
+ <manifest>
+ <attribute name="Main-Class" value="net.jcornell.answer_check.Main" />
+ </manifest>
+ </jar>
+ </target>
+</project>
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.util.ArrayList;
+import java.awt.Component;
+
+import net.jcornell.answer_check.KeyListController.KeyModel;
+import net.jcornell.answer_check.score_entry.ScoreEntryView;
+
+
+public class CompareController {
+ public final KeyModel key;
+ public final CompareScoreEntryController entryController;
+ protected final CompareView view;
+
+ public CompareController(KeyModel key, Component dialogParent) {
+ this.key = key;
+ ScoreEntryView entryView = new CompareEntryView();
+ entryController = new CompareScoreEntryController(
+ this,
+ new ArrayList<>(),
+ entryView
+ );
+ entryView.controller = entryController;
+
+ view = new CompareView(this, dialogParent);
+ }
+
+ public void init() {
+ entryController.init();
+ }
+
+ public void doCompare() {
+ view.doCompare();
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.util.List;
+import net.jcornell.answer_check.score_entry.ScoreEntryController;
+import net.jcornell.answer_check.score_entry.ScoreEntryView;
+
+
+public class CompareScoreEntryController extends ScoreEntryController {
+ protected final CompareController parent;
+
+ public CompareScoreEntryController(CompareController parent, List<String> contents, ScoreEntryView view) {
+ super(contents, view);
+ this.parent = parent;
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.awt.Dimension;
+import java.awt.Color;
+import javax.swing.text.Document;
+import java.util.List;
+import java.awt.Component;
+import javax.swing.text.BadLocationException;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JDialog;
+import javax.swing.JButton;
+import javax.swing.JScrollPane;
+import java.awt.Dialog.ModalityType;
+
+import net.jcornell.answer_check.KeyListController.KeyModel;
+import net.jcornell.answer_check.score_entry.ScoreEntryView;
+import net.jcornell.answer_check.util.GbcBuilder;
+import net.jcornell.answer_check.util.DocumentListenerAdapter;
+
+
+public class CompareView {
+ protected final CompareController controller;
+ protected final Component dialogParent;
+
+ public CompareView(CompareController controller, Component dialogParent) {
+ this.controller = controller;
+ this.dialogParent = dialogParent;
+ }
+
+ public void doCompare() {
+ JButton doneButton = new JButton("Done");
+ JOptionPane optionPane = new JOptionPane(
+ new JScrollPane(controller.entryController.view.container),
+ JOptionPane.PLAIN_MESSAGE,
+ JOptionPane.OK_OPTION,
+ null,
+ new JButton[] {doneButton}
+ );
+ JDialog dialog = optionPane.createDialog(dialogParent, "Compare Answers");
+ doneButton.addActionListener(ev -> {
+ dialog.setVisible(false);
+ dialog.dispose();
+ });
+
+ dialog.setModalityType(ModalityType.MODELESS);
+ dialog.setResizable(true);
+ dialog.setSize(dialog.getSize().width, 300);
+ dialog.setVisible(true);
+ }
+}
+
+
+class CompareEntryView extends ScoreEntryView {
+ protected static final Color TRANSPARENT = new Color(0, 0, 0, 0);
+
+ @Override
+ protected void finishRow(int ord, Document scoreEntryDoc) {
+ JLabel indicatorLabel = new JLabel();
+ indicatorLabel.setPreferredSize(new Dimension(10, 10));
+ indicatorLabel.setOpaque(true);
+ indicatorLabel.setBorder(javax.swing.BorderFactory.createLineBorder(Color.black));
+
+ scoreEntryDoc.addDocumentListener(new DocumentListenerAdapter(ev -> {
+ // TODO would be nice to use a generic `CompareEntryController' here instead of casting
+ List<String> answers = ((CompareScoreEntryController) controller).parent.key.answers;
+ String found;
+ try {
+ found = scoreEntryDoc.getText(0, scoreEntryDoc.getLength());
+ } catch (BadLocationException e) {
+ throw new RuntimeException(e);
+ }
+ if (ord < answers.size() && !found.isEmpty()) {
+ String expected = answers.get(ord);
+ Color color = found.equals(expected) ? Color.green : Color.red;
+ indicatorLabel.setBackground(color);
+ indicatorLabel.setOpaque(true);
+ } else {
+ indicatorLabel.setOpaque(false);
+ }
+ indicatorLabel.repaint();
+ }));
+
+ contentPanel.add(
+ indicatorLabel,
+ (new GbcBuilder()
+ .gridy(ord)
+ .value
+ )
+ );
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.List;
+
+import net.jcornell.answer_check.KeyListController.KeyModel;
+import net.jcornell.answer_check.score_entry.ScoreEntryController;
+import net.jcornell.answer_check.score_entry.ScoreEntryView;
+
+
+public class KeyEditModel {
+ protected final KeyModel target;
+ protected final ScoreEntryController entryController;
+ protected final KeyEditView view;
+
+ public KeyEditModel(KeyModel target, Component dialogParent) {
+ // Make a copy of the key and turn it over to the editor view. When the user is done
+ // editing, discard the changes or write them back to the target as appropriate.
+ this.target = target;
+
+ List<String> buffer = new ArrayList<>(target.answers);
+ ScoreEntryView entryView = new ScoreEntryView();
+ entryController = new ScoreEntryController(buffer, entryView);
+ entryView.controller = entryController;
+ entryController.init();
+
+ view = new KeyEditView(this, entryView, dialogParent);
+ }
+
+ public void doEdit() {
+ view.doEdit();
+ }
+
+ public void commit() {
+ target.name = view.getName();
+ target.answers = entryController.cleanContents();
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.awt.Component;
+import java.awt.GridBagConstraints;
+import java.awt.Dimension;
+import javax.swing.JDialog;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+
+import net.jcornell.answer_check.score_entry.ScoreEntryView;
+import net.jcornell.answer_check.util.GbcBuilder;
+
+
+public class KeyEditView {
+ public final KeyEditModel model;
+ protected final ScoreEntryView entryView;
+ protected final JTextField nameField;
+ public final JPanel container;
+ protected final Component dialogParent;
+
+ public KeyEditView(KeyEditModel model, ScoreEntryView entryView, Component dialogParent) {
+ this.model = model;
+ this.entryView = entryView;
+ nameField = new JTextField(model.target.name);
+ container = buildUi();
+ this.dialogParent = dialogParent;
+ }
+
+ protected JPanel buildUi() {
+ JPanel panel = new JPanel();
+ panel.setLayout(new GridBagLayout());
+ JScrollPane entryPanel = new JScrollPane(entryView.container);
+
+ panel.add(
+ nameField,
+ (new GbcBuilder()
+ .fill(GridBagConstraints.HORIZONTAL)
+ .insets(new Insets(0, 0, 5, 0))
+ .weightx(1.0)
+ .value
+ )
+ );
+ panel.add(
+ entryPanel,
+ (new GbcBuilder()
+ .gridx(0)
+ .fill(GridBagConstraints.BOTH)
+ .weightx(1.0)
+ .weighty(1.0)
+ .value
+ )
+ );
+ return panel;
+ }
+
+ public String getName() {
+ return nameField.getText();
+ }
+
+ public void doEdit() {
+ JOptionPane optionPane = new JOptionPane(
+ container,
+ JOptionPane.PLAIN_MESSAGE,
+ JOptionPane.OK_CANCEL_OPTION
+ );
+ JDialog dialog = optionPane.createDialog(dialogParent, "Answer Key Editor");
+ dialog.setResizable(true);
+ dialog.setSize(dialog.getSize().width, 300);
+ dialog.setVisible(true);
+ Integer result = (Integer) optionPane.getValue();
+ if (result != null && result == JOptionPane.OK_OPTION) {
+ model.commit();
+ }
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.ArrayList;
+import java.io.Serializable;
+import javax.swing.DefaultListModel;
+
+import net.jcornell.answer_check.score_entry.ScoreEntryController;
+import net.jcornell.answer_check.util.DataSave;
+
+
+public class KeyListController {
+ public static class KeyModel implements Serializable {
+ public String name;
+ public List<String> answers;
+
+ public KeyModel(String name, List<String> answers) {
+ this.name = name;
+ this.answers = answers;
+ }
+
+ public String toString() {
+ return name;
+ }
+ }
+
+ public final DefaultListModel<KeyModel> keys;
+ protected final KeyListView view;
+
+ public KeyListController(List<KeyModel> keys) {
+ this.keys = new DefaultListModel<>();
+ keys.forEach(this.keys::addElement);
+ view = new KeyListView(this);
+ }
+
+ protected void saveData() {
+ DataSave.saveData(Collections.list(keys.elements()));
+ }
+
+ public void addKey() {
+ KeyModel m = new KeyModel("New key", new ArrayList<>());
+ keys.addElement(m);
+ saveData();
+ }
+
+ public void removeKeyAt(int index) {
+ keys.remove(index);
+ saveData();
+ }
+
+ public void doEdit() {
+ KeyEditModel editModel = new KeyEditModel(view.getSelected(), view.container);
+ editModel.doEdit();
+ saveData();
+ view.onEditComplete();
+ }
+
+ public void doCompare() {
+ CompareController compareController = new CompareController(view.getSelected(), view.container);
+ compareController.init();
+ compareController.doCompare();
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import java.awt.Container;
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.util.Arrays;
+import javax.swing.GroupLayout.Alignment;
+import javax.swing.GroupLayout;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JButton;
+import javax.swing.ListSelectionModel;
+import javax.swing.JScrollPane;
+import java.awt.Dimension;
+import java.awt.Component;
+import javax.swing.BoxLayout;
+import javax.swing.Box;
+
+import net.jcornell.answer_check.KeyListController.KeyModel;
+import net.jcornell.answer_check.util.GbcBuilder;
+
+
+public class KeyListView {
+ protected final KeyListController controller;
+ public final Container container;
+ protected final JList<KeyModel> list;
+ protected final JButton editBtn, dropBtn, compareBtn;
+
+ public KeyListView(KeyListController controller) {
+ this.controller = controller;
+ list = new JList<>(controller.keys);
+ editBtn = new JButton("Edit…");
+ dropBtn = new JButton("Delete");
+ compareBtn = new JButton("Compare…");
+ container = buildUi();
+ refreshButtonState();
+ }
+
+ protected Container buildUi() {
+ list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ list.addListSelectionListener(ev -> refreshButtonState());
+ JScrollPane listPane = new JScrollPane(list);
+
+ JButton addBtn = new JButton("Add");
+ addBtn.addActionListener(ev -> {
+ controller.addKey();
+ list.setSelectedIndex(controller.keys.size() - 1);
+ });
+
+ editBtn.addActionListener(ev -> controller.doEdit());
+
+ dropBtn.addActionListener(ev -> controller.removeKeyAt(list.getSelectedIndex()));
+
+ compareBtn.addActionListener(ev -> controller.doCompare());
+
+ JPanel buttonBar = new JPanel();
+ Arrays.stream(new Component[] {addBtn, editBtn, dropBtn, compareBtn}).forEach(buttonBar::add);
+
+ JPanel container = new JPanel();
+ GridBagLayout layout = new GridBagLayout();
+ container.setLayout(layout);
+
+ container.add(
+ listPane,
+ (
+ new GbcBuilder()
+ .gridx(0)
+ .gridy(0)
+ .fill(GridBagConstraints.BOTH)
+ .weightx(1.0)
+ .weighty(1.0)
+ .value
+ )
+ );
+ container.add(
+ buttonBar,
+ (
+ new GbcBuilder()
+ .gridx(0)
+ .gridy(1)
+ .value
+ )
+ );
+
+ return container;
+ }
+
+ protected void refreshButtonState() {
+ boolean keySelected = list.getSelectedIndex() != -1;
+ for (JButton b : new JButton[] {editBtn, dropBtn, compareBtn}) {
+ b.setEnabled(keySelected);
+ }
+ }
+
+ protected KeyModel getSelected() {
+ return list.getSelectedValue();
+ }
+
+ public void onEditComplete() {
+ list.repaint();
+ }
+}
--- /dev/null
+package net.jcornell.answer_check;
+
+import javax.swing.JFrame;
+import java.util.List;
+import java.util.ArrayList;
+
+import net.jcornell.answer_check.KeyListController.KeyModel;
+import net.jcornell.answer_check.util.DataSave;
+
+
+public class Main {
+ public static void main(String[] args) {
+ List<KeyModel> keys;
+ try {
+ keys = DataSave.loadData();
+ } catch (RuntimeException e) {
+ keys = new ArrayList<>();
+ }
+
+ KeyListController c = new KeyListController(keys);
+ JFrame frame = new JFrame("Manage Keys");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setContentPane(c.view.container);
+ frame.pack();
+ frame.setVisible(true);
+ }
+}
--- /dev/null
+package net.jcornell.answer_check.score_entry;
+
+import java.util.Stack;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.IntStream;
+import java.util.stream.Collectors;
+
+
+public class ScoreEntryController {
+ public final List<String> contents;
+ public final ScoreEntryView view;
+
+ public ScoreEntryController(List<String> contents, ScoreEntryView view) {
+ this.contents = contents;
+ this.view = view;
+ }
+
+ public void init() {
+ IntStream.range(0, contents.size()).forEach(ord -> view.populateRow(ord));
+ view.init();
+ }
+
+ public void update(int index, String newValue) {
+ if (index < contents.size()) {
+ contents.set(index, newValue);
+ } else {
+ assert index == contents.size();
+ contents.add(newValue);
+ }
+ }
+
+ public List<String> cleanContents() {
+ Stack<String> copy = contents.stream().collect(Collectors.toCollection(Stack::new));
+ while (!copy.isEmpty() && copy.peek().isEmpty()) {
+ copy.pop();
+ }
+ return copy;
+ }
+}
--- /dev/null
+package net.jcornell.answer_check.score_entry;
+
+import java.awt.GridBagLayout;
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+import javax.swing.text.PlainDocument;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.AttributeSet;
+import java.awt.event.ActionListener;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.text.Document;
+import javax.swing.event.DocumentListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import javax.swing.event.DocumentEvent;
+
+import net.jcornell.answer_check.util.GbcBuilder;
+import net.jcornell.answer_check.util.DocumentListenerAdapter;
+
+
+public class ScoreEntryView {
+ protected static class ScoreDocument extends PlainDocument {
+ @Override
+ public void insertString(int offset, String toInsert, AttributeSet attrs) throws BadLocationException {
+ if (toInsert != null && getLength() == 0) {
+ super.insertString(offset, toInsert.toUpperCase(), attrs);
+ }
+ }
+ }
+
+ protected final FocusListener focusListener = new FocusListener() {
+ @Override public void focusGained(FocusEvent e) {
+ JTextField target = (JTextField) e.getComponent();
+ contentPanel.scrollRectToVisible(target.getBounds());
+ target.selectAll();
+ }
+
+ @Override public void focusLost(FocusEvent e) {}
+ };
+
+ public ScoreEntryController controller;
+ protected final JPanel contentPanel;
+ public final JPanel container;
+ protected final Map<Integer, JTextField> fromOrd;
+ protected final Map<JTextField, Integer> toOrd;
+ protected final Map<Document, JTextField> fieldsByDoc;
+ protected final DocumentListener modelUpdateListener, rowFillListener;
+
+ public ScoreEntryView() {
+ fromOrd = new HashMap<>();
+ toOrd = new HashMap<>();
+ fieldsByDoc = new HashMap<>();
+ modelUpdateListener = new DocumentListenerAdapter(ev -> {
+ JTextField source = fieldsByDoc.get(ev.getDocument());
+ controller.update(toOrd.get(source), source.getText());
+ });
+ rowFillListener = new DocumentListenerAdapter(ev -> {
+ JTextField target = fieldsByDoc.get(ev.getDocument());
+ int ord = toOrd.get(target);
+ if (ord == fromOrd.size() - 1) {
+ addNewRow();
+ }
+ if (!target.getText().isEmpty()) {
+ fromOrd.get(ord + 1).requestFocus();
+ }
+ });
+
+ container = new JPanel();
+ container.setLayout(new GridBagLayout());
+
+ contentPanel = new JPanel();
+ contentPanel.setLayout(new GridBagLayout());
+
+ container.add(contentPanel, new GridBagConstraints());
+ // add expanding box to push content to the top left
+ container.add(
+ new JPanel(),
+ (new GbcBuilder()
+ .gridx(1)
+ .gridy(1)
+ .weightx(1.0)
+ .weighty(1.0)
+ .value
+ )
+ );
+ }
+
+ public void init() {
+ addNewRow();
+ }
+
+ protected void finishRow(int ord, Document scoreEntryDoc) {}
+
+ protected void rowAddImpl(int ord, String initialValue) {
+ JLabel ordLabel = new JLabel(Integer.toString(ord + 1));
+ Document doc = new ScoreDocument();
+ JTextField textBox = new JTextField(doc, initialValue, 2);
+
+ fromOrd.put(ord, textBox);
+ toOrd.put(textBox, ord);
+ fieldsByDoc.put(doc, textBox);
+
+ doc.addDocumentListener(modelUpdateListener);
+ doc.addDocumentListener(rowFillListener);
+ textBox.addFocusListener(focusListener);
+
+ contentPanel.add(
+ ordLabel,
+ (new GbcBuilder()
+ .gridy(ord)
+ .value
+ )
+ );
+ contentPanel.add(
+ textBox,
+ (new GbcBuilder()
+ .gridy(ord)
+ .insets(new Insets(0, 10, 0, 10))
+ .value
+ )
+ );
+
+ finishRow(ord, doc);
+ contentPanel.revalidate();
+ }
+
+ protected void addNewRow() {
+ rowAddImpl(fromOrd.size(), "");
+ }
+
+ protected void populateRow(int ord) {
+ rowAddImpl(ord, controller.contents.get(ord));
+ }
+}
--- /dev/null
+package net.jcornell.answer_check.util;
+
+import java.nio.file.Path;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.file.Paths;
+import java.nio.file.Files;
+import java.util.List;
+
+import net.jcornell.answer_check.KeyListController.KeyModel;
+
+
+public class DataSave {
+ public static final Path SAVE_LOCATION = Paths.get(
+ System.getProperty("user.home"), "Library", "Application Support", "answer_check.ser"
+ );
+
+ private DataSave() {}
+
+ public static void saveData(List<KeyModel> data) {
+ try (OutputStream output = Files.newOutputStream(SAVE_LOCATION)) {
+ new ObjectOutputStream(output).writeObject(data);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static List<KeyModel> loadData() {
+ try {
+ List<KeyModel> keys;
+ try (InputStream input = Files.newInputStream(SAVE_LOCATION)) {
+ @SuppressWarnings("unchecked")
+ List<KeyModel> keys_ = (List<KeyModel>) new ObjectInputStream(input).readObject();
+ keys = keys_;
+ }
+ return keys;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
--- /dev/null
+package net.jcornell.answer_check.util;
+
+import java.util.function.Consumer;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+
+public class DocumentListenerAdapter implements DocumentListener {
+ protected final Consumer<DocumentEvent> upstream;
+
+ public DocumentListenerAdapter(Consumer<DocumentEvent> upstream) {
+ this.upstream = upstream;
+ }
+
+ @Override public void changedUpdate(DocumentEvent e) {
+ upstream.accept(e);
+ }
+
+ @Override public void insertUpdate(DocumentEvent e) {
+ upstream.accept(e);
+ }
+
+ @Override public void removeUpdate(DocumentEvent e) {
+ upstream.accept(e);
+ }
+}
--- /dev/null
+package net.jcornell.answer_check.util;
+
+import java.awt.GridBagConstraints;
+import java.awt.Insets;
+
+
+public class GbcBuilder {
+ public final GridBagConstraints value = new GridBagConstraints();
+
+ public GbcBuilder gridx(int value) {
+ this.value.gridx = value;
+ return this;
+ }
+
+ public GbcBuilder gridy(int value) {
+ this.value.gridy = value;
+ return this;
+ }
+
+ public GbcBuilder fill(int value) {
+ this.value.fill = value;
+ return this;
+ }
+
+ public GbcBuilder insets(Insets value) {
+ this.value.insets = value;
+ return this;
+ }
+
+ public GbcBuilder anchor(int value) {
+ this.value.anchor = value;
+ return this;
+ }
+
+ public GbcBuilder weightx(double value) {
+ this.value.weightx = value;
+ return this;
+ }
+
+ public GbcBuilder weighty(double value) {
+ this.value.weighty = value;
+ return this;
+ }
+}