package uk.co.bytemark.vm.enigma.inquisition.gui.editor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Arrays; import javax.swing.DefaultListModel; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.text.Document; import uk.co.bytemark.vm.enigma.inquisition.gui.screens.editor.AbstractQuestionEditorMainPanel; import uk.co.bytemark.vm.enigma.inquisition.misc.SimpleDocumentListener; import uk.co.bytemark.vm.enigma.inquisition.questions.DragAndDropQuestion; import uk.co.bytemark.vm.enigma.inquisition.questions.MultipleChoiceQuestion; import uk.co.bytemark.vm.enigma.inquisition.questions.Question; import uk.co.bytemark.vm.enigma.inquisition.questions.QuestionSet; import uk.co.bytemark.vm.enigma.inquisition.questions.QuestionType; import uk.co.bytemark.vm.enigma.inquisition.questions.QuestionSet.QuestionSetBuilder; public class QuestionEditorMainPanel extends AbstractQuestionEditorMainPanel { private static final int DEFAULT_RECOMMENDED_TIME = 120; private static final String DEFAULT_DESCRIPTION; static { DEFAULT_DESCRIPTION = "Questions on ...\n" + "<hr>\n" + "<b>Info:</b><br>\n" + "<ul>\n" + "<li>Maintainer: Joe Bloggs <tt><joebloggs@somewhere.com></tt>\n" + "<li>Home page: <a href=\"http://website.com\">http://website.com</a>\n" + "<li>Version: 2\n" + "<li>Date published: 13/October/2008\n" + "<li>Licence: (Public domain, Creative Commons etc)\n" + "</ul>\n"; } private static final String DEFAULT_MULTIPLE_CHOICE_QUESTION_TEXT; static { DEFAULT_MULTIPLE_CHOICE_QUESTION_TEXT = "This can include Java code with automatic syntax highlighting, e.g.\n<java>\n" + " // Java code...\n public static void main(String[] args) { ... }\n</java>"; } private static final String DEFAULT_MULTIPLE_CHOICE_EXPLANATION_TEXT; static { DEFAULT_MULTIPLE_CHOICE_EXPLANATION_TEXT = "The explanation can reference the options directly, " + "e.g., @1@, @2@ and @3@, or all the correct options, with @allcorrect@."; } private static final String DEFAULT_DRAG_AND_DROP_CHOICE_QUESTION_TEXT; static { DEFAULT_DRAG_AND_DROP_CHOICE_QUESTION_TEXT = // "Complete the following Java class:\n" + // "<CopyToExplanation>\n" + // "<java>\n" + // "public class <slot>Foobar</slot> {\n" + // " private final <slot>int</slot> field;\n" + // "}" + // "</java>\n" + // "</CopyToExplanation>\n"; } private static final String DEFAULT_DRAG_AND_DROP_CHOICE_EXPLANATION_TEXT; static { DEFAULT_DRAG_AND_DROP_CHOICE_EXPLANATION_TEXT = // "The correct answer is:\n" + // "<CopyFromQuestion/>\n" + // "(The appropriate section gets copied from the question, but with the slots filled in correctly)"; } private static final String DEFAULT_NAME = "Name"; private static final String DEFAULT_CATEGORY = "Category:Subcategory"; private boolean suppressDocumentDirtyingListeners = false; private DirtyingActionListener dirtyingActionListener = new DirtyingActionListener() { public void dirtyingActionHappened() { // Do nothing } }; public QuestionEditorMainPanel() { setUpNewQuestionSet(); moveUpButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { moveQuestionListItem(questionJList.getSelectedIndex(), questionJList.getSelectedIndex() - 1); } }); moveDownButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { moveQuestionListItem(questionJList.getSelectedIndex(), questionJList.getSelectedIndex() + 1); } }); deleteButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { deleteSelectedQuestion(); } }); editButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { editSelectedQuestion(false); } }); questionJList.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { refreshQuestionButtonEnabledStates(); } }); addNewButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { addNewQuestion(); } }); previewButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { previewDescriptionHtml(); } }); questionJList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) { if (!questionJList.isSelectionEmpty()) { editSelectedQuestion(false); } } } }); recommendedTimeTextField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent evt) { int result; try { result = Integer.parseInt(recommendedTimeTextField.getText()); } catch (NumberFormatException e) { result = -1; } if (result <= 0) recommendedTimeTextField.setText(Integer.toString(DEFAULT_RECOMMENDED_TIME)); } }); nameTextField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { if (isWhitespace(nameTextField)) nameTextField.setText(DEFAULT_NAME); } }); categoryTextField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { if (isWhitespace(categoryTextField)) categoryTextField.setText(DEFAULT_CATEGORY); } }); addDirtyingListenerToDocument(nameTextField.getDocument()); addDirtyingListenerToDocument(recommendedTimeTextField.getDocument()); addDirtyingListenerToDocument(descriptionTextPane.getDocument()); addDirtyingListenerToDocument(categoryTextField.getDocument()); refreshQuestionButtonEnabledStates(); } private boolean isWhitespace(JTextField textField) { String text = textField.getText(); return text.matches("\\s*"); } private void previewDescriptionHtml() { JFrame frame = getParentFrame(); PreviewHtmlDialog dialog = new PreviewHtmlDialog(frame, descriptionTextPane.getText()); dialog.setSize(PreviewHtmlDialog.PREVIEW_HTML_DIALOG_DEFAULT_DIMENSION); dialog.setLocationRelativeTo(frame); dialog.setVisible(true); } private void addNewQuestion() { JFrame frame = getParentFrame(); ChooseQuestionTypeDialog dialog = new ChooseQuestionTypeDialog(frame); dialog.setSize(300, 240); dialog.setLocationRelativeTo(frame); dialog.setVisible(true); if (dialog.okSelected()) { QuestionType questionType = dialog.getQuestionType(); Question question; if (questionType == QuestionType.MULTIPLE_CHOICE) { question = new MultipleChoiceQuestion.Builder().questionText(DEFAULT_MULTIPLE_CHOICE_QUESTION_TEXT) .explanationText(DEFAULT_MULTIPLE_CHOICE_EXPLANATION_TEXT).option("Option 1", true).option( "Option 2", false).option("Option 3", false).build(); } else { // DRAG AND DROP question = new DragAndDropQuestion(DEFAULT_DRAG_AND_DROP_CHOICE_QUESTION_TEXT, DEFAULT_DRAG_AND_DROP_CHOICE_EXPLANATION_TEXT, Arrays.asList("Extra fragment"), false); } DefaultListModel model = (DefaultListModel) questionJList.getModel(); int insertIndex = questionJList.getSelectedIndex() + 1; model.add(insertIndex, question); questionJList.setSelectedIndex(insertIndex); editSelectedQuestion(true); } } private JFrame getParentFrame() { return (JFrame) SwingUtilities.getAncestorOfClass(JFrame.class, this); } private void editSelectedQuestion(boolean newQuestion) { final int index = questionJList.getSelectedIndex(); Question question = getQuestion(index); JFrame frame = getParentFrame(); JPanel questionEditorPanel; String title; if (question instanceof MultipleChoiceQuestion) { MultipleChoiceQuestion multipleChoiceQuestion = (MultipleChoiceQuestion) question; questionEditorPanel = new MultipleChoiceQuestionEditorPanel(multipleChoiceQuestion); title = "Multiple Choice Question"; } else if (question instanceof DragAndDropQuestion) { DragAndDropQuestion dragAndDropQuestion = (DragAndDropQuestion) question; questionEditorPanel = new DragAndDropQuestionEditorPanel(dragAndDropQuestion); title = "Drag and Drop Question"; } else { throw new AssertionError("Unexpected type of question"); } QuestionDialog questionDialog = new QuestionDialog(frame, title, questionEditorPanel); questionDialog.pack(); questionDialog.setLocationRelativeTo(frame); questionDialog.setVisible(true); if (questionDialog.okSelected()) { Question replacementQuestion = questionDialog.getQuestion(); if (replacementQuestion.equals(question)) { if (newQuestion) dirtyingActionListener.dirtyingActionHappened(); } else { replaceQuestion(index, replacementQuestion); questionJList.setSelectedIndex(index); dirtyingActionListener.dirtyingActionHappened(); } } else { // Cancelled if (newQuestion) { DefaultListModel model = (DefaultListModel) questionJList.getModel(); model.remove(index); selectNearestQuestion(index); } } } private void replaceQuestion(final int index, Question replacementQuestion) { DefaultListModel model = (DefaultListModel) questionJList.getModel(); model.remove(index); model.add(index, replacementQuestion); } private Question getQuestion(int index) { return (Question) questionJList.getModel().getElementAt(index); } private void addDirtyingListenerToDocument(Document document) { document.addDocumentListener(new SimpleDocumentListener() { @Override public void documentChanged(DocumentEvent e) { if (!suppressDocumentDirtyingListeners) dirtyingActionListener.dirtyingActionHappened(); } }); } private void deleteSelectedQuestion() { int deleteIndex = questionJList.getSelectedIndex(); DefaultListModel model = (DefaultListModel) questionJList.getModel(); model.remove(deleteIndex); dirtyingActionListener.dirtyingActionHappened(); selectNearestQuestion(deleteIndex); } private void selectNearestQuestion(int index) { DefaultListModel model = (DefaultListModel) questionJList.getModel(); if (!model.isEmpty()) questionJList.setSelectedIndex(Math.max(0, index - 1)); } private void refreshQuestionButtonEnabledStates() { int index = questionJList.getSelectedIndex(); boolean itemSelected = index >= 0; if (itemSelected) { deleteButton.setEnabled(true); editButton.setEnabled(true); boolean firstItemSelected = index == 0; moveUpButton.setEnabled(!firstItemSelected); boolean lastItemSelected = index == questionJList.getModel().getSize() - 1; moveDownButton.setEnabled(!lastItemSelected); } else { deleteButton.setEnabled(false); editButton.setEnabled(false); moveUpButton.setEnabled(false); moveDownButton.setEnabled(false); } } private void moveQuestionListItem(int currentPosition, int newPosition) { DefaultListModel model = (DefaultListModel) questionJList.getModel(); Object item = model.remove(currentPosition); model.add(newPosition, item); questionJList.setSelectedIndex(newPosition); dirtyingActionListener.dirtyingActionHappened(); } public void setUpNewQuestionSet() { suppressDocumentDirtyingListeners = true; nameTextField.setText(""); categoryTextField.setText(""); recommendedTimeTextField.setText(Integer.toString(DEFAULT_RECOMMENDED_TIME)); nameTextField.setText(DEFAULT_NAME); categoryTextField.setText(DEFAULT_CATEGORY); descriptionTextPane.setText(DEFAULT_DESCRIPTION); descriptionTextPane.setCaretPosition(0); questionJList.setModel(new DefaultListModel()); suppressDocumentDirtyingListeners = false; } public void setQuestionSet(QuestionSet questionSet) { suppressDocumentDirtyingListeners = true; nameTextField.setText(questionSet.getName()); categoryTextField.setText(questionSet.getCategorySequence()); recommendedTimeTextField.setText(Integer.toString(questionSet.getRecommendedTimePerQuestion())); descriptionTextPane.setText(questionSet.getDescription()); descriptionTextPane.setCaretPosition(0); DefaultListModel questionsListModel = new DefaultListModel(); for (Question question : questionSet.getQuestions()) { questionsListModel.addElement(question); } questionJList.setModel(questionsListModel); suppressDocumentDirtyingListeners = false; } public QuestionSet getQuestionSet() { QuestionSetBuilder questionSetBuilder = new QuestionSet.QuestionSetBuilder(); int numberOfQuestions = getNumberOfQuestions(); for (int i = 0; i < numberOfQuestions; i++) questionSetBuilder.addQuestion(getQuestion(i)); int recommendedTimePerQuestion; try { recommendedTimePerQuestion = Integer.parseInt(recommendedTimeTextField.getText()); } catch (NumberFormatException e) { recommendedTimePerQuestion = -1; } if (recommendedTimePerQuestion < 0) recommendedTimePerQuestion = DEFAULT_RECOMMENDED_TIME; String name = nameTextField.getText(); String description = descriptionTextPane.getText(); String category = categoryTextField.getText(); return questionSetBuilder.name(name).category(category).description(description).recommendedTime( recommendedTimePerQuestion).build(); } private int getNumberOfQuestions() { int numberOfQuestions = questionJList.getModel().getSize(); return numberOfQuestions; } public void setDirtyingActionListener(DirtyingActionListener dirtyingActionListener) { this.dirtyingActionListener = dirtyingActionListener; } }