From: Jakob Cornell Date: Sun, 13 Sep 2020 00:37:57 +0000 (-0500) Subject: Initial commit X-Git-Url: https://jcornell.net/gitweb/gitweb.cgi?a=commitdiff_plain;ds=inline;p=answer-check.git Initial commit --- a1b73551ec7e184def3faa56183cfa4f1858adca diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2e7327 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/out diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..57a7cd6 --- /dev/null +++ b/build.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/net/jcornell/answer_check/CompareController.java b/src/net/jcornell/answer_check/CompareController.java new file mode 100644 index 0000000..8dd4195 --- /dev/null +++ b/src/net/jcornell/answer_check/CompareController.java @@ -0,0 +1,35 @@ +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(); + } +} diff --git a/src/net/jcornell/answer_check/CompareScoreEntryController.java b/src/net/jcornell/answer_check/CompareScoreEntryController.java new file mode 100644 index 0000000..6102324 --- /dev/null +++ b/src/net/jcornell/answer_check/CompareScoreEntryController.java @@ -0,0 +1,15 @@ +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 contents, ScoreEntryView view) { + super(contents, view); + this.parent = parent; + } +} diff --git a/src/net/jcornell/answer_check/CompareView.java b/src/net/jcornell/answer_check/CompareView.java new file mode 100644 index 0000000..dce727c --- /dev/null +++ b/src/net/jcornell/answer_check/CompareView.java @@ -0,0 +1,92 @@ +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 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 + ) + ); + } +} diff --git a/src/net/jcornell/answer_check/KeyEditModel.java b/src/net/jcornell/answer_check/KeyEditModel.java new file mode 100644 index 0000000..2a56a45 --- /dev/null +++ b/src/net/jcornell/answer_check/KeyEditModel.java @@ -0,0 +1,39 @@ +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 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(); + } +} diff --git a/src/net/jcornell/answer_check/KeyEditView.java b/src/net/jcornell/answer_check/KeyEditView.java new file mode 100644 index 0000000..873acb0 --- /dev/null +++ b/src/net/jcornell/answer_check/KeyEditView.java @@ -0,0 +1,79 @@ +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(); + } + } +} diff --git a/src/net/jcornell/answer_check/KeyListController.java b/src/net/jcornell/answer_check/KeyListController.java new file mode 100644 index 0000000..963f811 --- /dev/null +++ b/src/net/jcornell/answer_check/KeyListController.java @@ -0,0 +1,64 @@ +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 answers; + + public KeyModel(String name, List answers) { + this.name = name; + this.answers = answers; + } + + public String toString() { + return name; + } + } + + public final DefaultListModel keys; + protected final KeyListView view; + + public KeyListController(List 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(); + } +} diff --git a/src/net/jcornell/answer_check/KeyListView.java b/src/net/jcornell/answer_check/KeyListView.java new file mode 100644 index 0000000..27affa6 --- /dev/null +++ b/src/net/jcornell/answer_check/KeyListView.java @@ -0,0 +1,102 @@ +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 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(); + } +} diff --git a/src/net/jcornell/answer_check/Main.java b/src/net/jcornell/answer_check/Main.java new file mode 100644 index 0000000..5c8a176 --- /dev/null +++ b/src/net/jcornell/answer_check/Main.java @@ -0,0 +1,27 @@ +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 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); + } +} diff --git a/src/net/jcornell/answer_check/score_entry/ScoreEntryController.java b/src/net/jcornell/answer_check/score_entry/ScoreEntryController.java new file mode 100644 index 0000000..c39d6e8 --- /dev/null +++ b/src/net/jcornell/answer_check/score_entry/ScoreEntryController.java @@ -0,0 +1,40 @@ +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 contents; + public final ScoreEntryView view; + + public ScoreEntryController(List 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 cleanContents() { + Stack copy = contents.stream().collect(Collectors.toCollection(Stack::new)); + while (!copy.isEmpty() && copy.peek().isEmpty()) { + copy.pop(); + } + return copy; + } +} diff --git a/src/net/jcornell/answer_check/score_entry/ScoreEntryView.java b/src/net/jcornell/answer_check/score_entry/ScoreEntryView.java new file mode 100644 index 0000000..8ee9b94 --- /dev/null +++ b/src/net/jcornell/answer_check/score_entry/ScoreEntryView.java @@ -0,0 +1,142 @@ +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 fromOrd; + protected final Map toOrd; + protected final Map 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)); + } +} diff --git a/src/net/jcornell/answer_check/util/DataSave.java b/src/net/jcornell/answer_check/util/DataSave.java new file mode 100644 index 0000000..a60edb4 --- /dev/null +++ b/src/net/jcornell/answer_check/util/DataSave.java @@ -0,0 +1,44 @@ +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 data) { + try (OutputStream output = Files.newOutputStream(SAVE_LOCATION)) { + new ObjectOutputStream(output).writeObject(data); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static List loadData() { + try { + List keys; + try (InputStream input = Files.newInputStream(SAVE_LOCATION)) { + @SuppressWarnings("unchecked") + List keys_ = (List) new ObjectInputStream(input).readObject(); + keys = keys_; + } + return keys; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/net/jcornell/answer_check/util/DocumentListenerAdapter.java b/src/net/jcornell/answer_check/util/DocumentListenerAdapter.java new file mode 100644 index 0000000..09d4750 --- /dev/null +++ b/src/net/jcornell/answer_check/util/DocumentListenerAdapter.java @@ -0,0 +1,26 @@ +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 upstream; + + public DocumentListenerAdapter(Consumer 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); + } +} diff --git a/src/net/jcornell/answer_check/util/GbcBuilder.java b/src/net/jcornell/answer_check/util/GbcBuilder.java new file mode 100644 index 0000000..72e9de9 --- /dev/null +++ b/src/net/jcornell/answer_check/util/GbcBuilder.java @@ -0,0 +1,44 @@ +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; + } +}