package uk.co.bytemark.vm.enigma.inquisition.gui.quiz; import java.awt.CardLayout; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.SwingUtilities; import uk.co.bytemark.vm.enigma.inquisition.gui.images.Icons; import uk.co.bytemark.vm.enigma.inquisition.gui.misc.QuizConfig; import uk.co.bytemark.vm.enigma.inquisition.gui.quizchooser.InquisitionMain; import uk.co.bytemark.vm.enigma.inquisition.gui.quizchooser.ReturnCallback; import uk.co.bytemark.vm.enigma.inquisition.gui.quizchooser.SuffixFileFilter; import uk.co.bytemark.vm.enigma.inquisition.gui.screens.AboutDialog; import uk.co.bytemark.vm.enigma.inquisition.gui.screens.AbstractQuizFrame; import uk.co.bytemark.vm.enigma.inquisition.misc.Constants; import uk.co.bytemark.vm.enigma.inquisition.misc.Utils; import uk.co.bytemark.vm.enigma.inquisition.questions.Answer; 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.QuestionSetManager; import uk.co.bytemark.vm.enigma.inquisition.quiz.QuizState; public class QuizFrame extends AbstractQuizFrame implements PropertyChangeListener { // Only touch quizState from the Swing thread private final QuizState quizState; private final QuestionPanelManager questionPanelManager; private final TransparentPane glassPane; private final ReturnCallback returnCallback; private boolean updating = false; private boolean showingExplanation = false; private String currentExplanation = null; private int previousSplitSize = -1; private QuizTimerThread quizTimerThread; public QuizFrame(final QuizState quizState, ReturnCallback returnCallback) { Utils.checkArgumentNotNull(quizState, "quizState"); Utils.checkArgumentNotNull(returnCallback, "returnCallback"); this.returnCallback = returnCallback; this.quizState = quizState; this.questionPanelManager = new QuestionPanelManager(); initializeGuiFurther(); glassPane = new TransparentPane(); setGlassPane(glassPane); setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); addEventHandlers(); if (quizState.isQuizTimed()) quizState.startTimer(); quizState.addPropertyChangeListener(this); launchQuizTimer(); updateFromModel(); } private void launchQuizTimer() { quizTimerThread = new QuizTimerThread(); quizTimerThread.start(); } private void initializeGuiFurther() { // Add a favicon thingy if (Icons.FAVICON.isAvailable()) setIconImage(Icons.FAVICON.getImage()); setTitle("Inquisition - " + quizState.getQuestionSetName()); // Respond dynamically to window resizes Toolkit.getDefaultToolkit().setDynamicLayout(true); if (Icons.FIRST.isAvailable()) getFirstQuestionButton().setIcon(Icons.FIRST.getIcon()); if (Icons.BACK.isAvailable()) getPreviousQuestionButton().setIcon(Icons.BACK.getIcon()); if (Icons.FORWARD.isAvailable()) getNextQuestionButton().setIcon(Icons.FORWARD.getIcon()); if (Icons.LAST.isAvailable()) getLastQuestionButton().setIcon(Icons.LAST.getIcon()); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); getExplanationTextPane().setEditorKit(new ExplanationPanelHTMLEditorKit()); } private void updateFromModel() { if (updating) throw new IllegalStateException("Attempting to update while already updating"); updating = true; try { updateQuestionNavigationComboBox(); getFirstQuestionButton().setEnabled(!quizState.isFirstQuestion()); getPreviousQuestionButton().setEnabled(!quizState.isFirstQuestion()); getNextQuestionButton().setEnabled(!quizState.isLastQuestion()); getLastQuestionButton().setEnabled(!quizState.isLastQuestion()); getFindNextMarkedButton().setEnabled(quizState.isAnyQuestionMarked()); getFindNextNextUnansweredButton().setEnabled(quizState.isAnyQuestionUnanswered()); getMarkedForReviewBox().setSelected(quizState.isMarked()); updateProgressBar(); updateTimerDisplay(); updateQuestion(); updateExplanationPanel(); } finally { updating = false; } } private void updateExplanationPanel() { if (quizState.isInExplanationMode()) { setRightWrongLabels(); // getCurrentQuestionPanel().enterReviewMode(); if (!showingExplanation) { getExplanationPanel().setVisible(true); getCheckAnswerButton().setText("Hide answer and explanation"); if (previousSplitSize == -1) getSplitPane().setDividerLocation(-1); else getSplitPane().setDividerLocation(previousSplitSize); getSplitPane().setDividerSize(5); showingExplanation = true; } String explanationText = quizState.getExplanationText(); if (!explanationText.equals(currentExplanation)) { String explanationText2 = questionPanelManager.getExplanationText(quizState.getQuestionNumber()); // getExplanationTextPane().setText(explanationText); // TODO: Fix this getExplanationTextPane().setText(explanationText2); getExplanationTextPane().setCaretPosition(0); currentExplanation = explanationText; } } else { getExplanationPanel().setVisible(false); getCheckAnswerButton().setText("Show answer and explanation"); if (showingExplanation) previousSplitSize = getSplitPane().getDividerLocation(); getSplitPane().setDividerLocation(1.0D); getSplitPane().setDividerSize(0); showingExplanation = false; } getPinExplanationPanelCheckBox().setSelected(quizState.keepInExplanationModeWhileNavigating()); getFindNextIncorrectAnswerButton().setEnabled(quizState.isAnyQuestionIncorrect()); } private void setRightWrongLabels() { JLabel rightOrWrongLabel = getRightOrWrongLabel(); if (quizState.isCorrect()) { if (Icons.TICK.isAvailable()) rightOrWrongLabel.setIcon(Icons.TICK.getIcon()); rightOrWrongLabel.setText("Correct"); } else { if (Icons.CROSS.isAvailable()) rightOrWrongLabel.setIcon(Icons.CROSS.getIcon()); rightOrWrongLabel.setText("Incorrect"); } } private void updateQuestion() { final int questionNumber = quizState.getQuestionNumber(); Question question = quizState.getQuestion(); if (!questionPanelManager.hasPanelFor(questionNumber) || !questionPanelManager.isPanelCorrectForQuestion(questionNumber, question)) { questionPanelManager.createQuestionPanel(this, questionNumber, question); } questionPanelManager.setAnswer(questionNumber, quizState.getAnswer()); questionPanelManager.showPanel(questionNumber); questionPanelManager.setExplanationMode(questionNumber, quizState.isInExplanationMode()); questionPanelManager.fixLayout(questionNumber); } private void updateTimerDisplay() { boolean isQuizTimed = quizState.isQuizTimed(); if (isQuizTimed) { if (quizState.isTimerExpired()) { getTimeRemainingTextField().setText("Time's up!"); getPauseButton().setVisible(false); } else { int timeRemaining = quizState.getTimeRemaining(); getTimeRemainingTextField().setText(formatTimeRemaining(timeRemaining)); getPauseButton().setVisible(true); } if (quizState.isTimerRunning()) { getPauseButton().setText("Pause"); } else { getPauseButton().setText("Restart"); } } else { getPauseButton().setVisible(false); } getTimeRemainingTextField().setVisible(isQuizTimed); getTimeRemainingLabel().setVisible(isQuizTimed); } private String formatTimeRemaining(int timeRemaining) { int myTimeRemaining = timeRemaining; final int hours = myTimeRemaining / (60 * 60); myTimeRemaining -= hours * 60 * 60; final int minutes = myTimeRemaining / 60; final int seconds = myTimeRemaining - minutes * 60; if (hours == 0) return String.format("%02d:%02d", minutes, seconds); else return String.format("%02d:%02d:%02d", hours, minutes, seconds); } private void updateProgressBar() { int questionsAnswered = quizState.getNumberOfAnsweredQuestions(); int numberOfQuestions = quizState.getNumberOfQuestions(); double percentage = 100.0 * questionsAnswered / numberOfQuestions; String percentageString = String.format(" (%.0f%%)", percentage); getAnsweredProgressBar().setString(questionsAnswered + " / " + numberOfQuestions + percentageString); getAnsweredProgressBar().setValue((int) percentage); } private void updateQuestionNavigationComboBox() { JComboBox navigateToQuestionComboBox = getNavigateToQuestionComboBox(); int numberOfQuestions = quizState.getNumberOfQuestions(); int previousSize = navigateToQuestionComboBox.getModel().getSize(); if (numberOfQuestions != previousSize) { String[] questionNumbers = new String[numberOfQuestions]; for (int i = 0; i < numberOfQuestions; i++) questionNumbers[i] = Integer.toString(i + 1); navigateToQuestionComboBox.setModel(new DefaultComboBoxModel(questionNumbers)); } navigateToQuestionComboBox.setSelectedIndex(quizState.getQuestionNumber() - 1); } public void propertyChange(PropertyChangeEvent evt) { updateFromModel(); } private void addEventHandlers() { getFirstQuestionButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToFirstQuestion(); } }); getPreviousQuestionButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToPreviousQuestion(); } }); getNextQuestionButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToNextQuestion(); } }); getLastQuestionButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToLastQuestion(); } }); getNavigateToQuestionComboBox().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (updating) return; int questionNumber = getNavigateToQuestionComboBox().getSelectedIndex() + 1; quizState.goToQuestion(questionNumber); } }); getFindNextIncorrectAnswerButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToNextIncorrect(); } }); getFindNextMarkedButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToNextMarked(); } }); getFindNextNextUnansweredButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.goToNextUnanswered(); } }); getMarkedForReviewBox().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.markCurrentQuestionForReview(getMarkedForReviewBox().isSelected()); } }); getPauseButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (quizState.isTimerRunning()) quizState.pauseTimer(); else quizState.startTimer(); } }); getCheckAnswerButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.toggleExplanationMode(); } }); getPinExplanationPanelCheckBox().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { quizState.setKeepInExplanationModeWhileNavigating(getPinExplanationPanelCheckBox().isSelected()); } }); getOverviewButton().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if (!quizState.isInExplanationMode()) quizState.toggleExplanationMode(); if (!quizState.keepInExplanationModeWhileNavigating()) quizState.setKeepInExplanationModeWhileNavigating(true); ResultsDialog resultsDialog = new ResultsDialog(QuizFrame.this, quizState); resultsDialog.setLocationRelativeTo(QuizFrame.this); resultsDialog.setVisible(true); } }); getSaveQuizMenuItem().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { boolean successful = doSaveQuizStateProtocol(); if (successful) endQuiz(); } }); getExitQuizMenuItem().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { doExitQuizProtocol(); } }); getAboutMenuItem().addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JDialog aboutDialog = new AboutDialog(QuizFrame.this, Constants.getAboutText()); aboutDialog.setLocationRelativeTo(QuizFrame.this); aboutDialog.setVisible(true); } }); addWindowListener(new java.awt.event.WindowAdapter() { @Override public void windowClosing(WindowEvent evt) { doExitQuizProtocol(); } }); addComponentListener(new ResizeListener()); } private boolean doSaveQuizStateProtocol() { boolean resumeTimerIfCancelled; if (quizState.isQuizTimed() && quizState.isTimerRunning()) { quizState.pauseTimer(); resumeTimerIfCancelled = true; } else { resumeTimerIfCancelled = false; } JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogTitle("Save In-Progress Quiz"); SuffixFileFilter.setSoleFileFilter(fileChooser, Constants.QUIZ_SUFFIX_FILE_FILTER); int resultStatus = fileChooser.showSaveDialog(QuizFrame.this); if (resultStatus != JFileChooser.APPROVE_OPTION) { if (resumeTimerIfCancelled) quizState.startTimer(); return false; } String quizStateFileName = fileChooser.getSelectedFile().getAbsolutePath(); File quizStateFile = new File(quizStateFileName); if (!Constants.QUIZ_SUFFIX_FILE_FILTER.accept(quizStateFile)) { quizStateFileName = Constants.QUIZ_SUFFIX_FILE_FILTER.addSuffixTo(quizStateFileName); quizStateFile = new File(quizStateFileName); } if (quizStateFile.exists()) { int confirmStatus = JOptionPane.showConfirmDialog(QuizFrame.this, "'" + quizStateFile.getAbsolutePath() + "' exists. Do you want to overwrite it?", "Confirm Overwrite", JOptionPane.OK_CANCEL_OPTION); if (confirmStatus != JOptionPane.OK_OPTION) { if (resumeTimerIfCancelled) quizState.startTimer(); return false; } } try { FileOutputStream fileOutputStream = new FileOutputStream(quizStateFileName); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(quizState); objectOutputStream.close(); quizState.setNotDirty(); } catch (IOException e) { JOptionPane.showMessageDialog(QuizFrame.this, "Could not save quiz '" + quizStateFileName + "' due to error '" + e.getMessage() + "'", "Could not save quiz", JOptionPane.ERROR_MESSAGE); return false; } return true; } public void setGlassPaneVisible(boolean visible) { glassPane.setVisible(visible); } private class QuizTimerThread extends Thread { private boolean keepRunning = true; @Override public void run() { while (keepRunning) { try { Thread.sleep(1000); } catch (InterruptedException e) { // Don't care } SwingUtilities.invokeLater(new Runnable() { public void run() { updateTimerDisplay(); } }); } } public void finish() { if (!keepRunning) throw new IllegalStateException("Not running"); keepRunning = false; interrupt(); } } private void endQuiz() { setVisible(false); quizTimerThread.finish(); dispose(); returnCallback.doReturn(); } private void doExitQuizProtocol() { if (quizState.isDirty()) { int saveStatus = JOptionPane.showConfirmDialog(QuizFrame.this, "Do you want to save the current quiz?", "End Quiz", JOptionPane.YES_NO_CANCEL_OPTION); if (saveStatus == JOptionPane.CANCEL_OPTION) return; else if (saveStatus == JOptionPane.YES_OPTION) { boolean successful = doSaveQuizStateProtocol(); if (!successful) return; } } endQuiz(); } private class QuestionPanelManager { private Map<Integer, QuestionPanel> questionPanels = new HashMap<Integer, QuestionPanel>(); private Map<Integer, Question> questions = new HashMap<Integer, Question>(); public boolean hasPanelFor(int questionNumber) { return questionPanels.containsKey(questionNumber); } public void setAnswer(int questionNumber, Answer answer) { questionPanels.get(questionNumber).setAnswer(answer); } // TODO: Don't want this public String getExplanationText(int questionNumber) { return questionPanels.get(questionNumber).getExplanationText(); } protected boolean isPanelCorrectForQuestion(int questionNumber, Question question) { return questions.containsKey(questionNumber); } public void fixLayout(int questionNumber) { QuestionPanel questionPanel = questionPanels.get(questionNumber); if (questionPanel instanceof MultipleChoicePanel) ((MultipleChoicePanel) questionPanel).fixDividerLocation(); } public void showPanel(int questionNumber) { JPanel questionPanelHolderPanel = getQuestionPanelHolderPanel(); CardLayout questionPanelLayout = (CardLayout) questionPanelHolderPanel.getLayout(); questionPanelLayout.show(questionPanelHolderPanel, Integer.toString(questionNumber)); } public void setPanel(int questionNumber, QuestionPanel panel, Question question) { JPanel questionPanelHolderPanel = getQuestionPanelHolderPanel(); questionPanelHolderPanel.add(panel, Integer.toString(questionNumber)); questionPanels.put(questionNumber, panel); questions.put(questionNumber, question); } public void setExplanationMode(int questionNumber, boolean inExplanationMode) { QuestionPanel questionPanel = questionPanels.get(questionNumber); if (inExplanationMode) questionPanel.enterReviewMode(); else questionPanel.enterQuestionMode(); } public void createQuestionPanel(final QuizFrame quizFrame, final int questionNumber, Question question) throws AssertionError { AnswerChangedObserver answerChangedObserver = new AnswerChangedObserver() { public void answerChanged(Answer answer) { if (quizFrame.quizState.getQuestionNumber() != questionNumber) throw new IllegalStateException("Should not be updating answer of non-current question"); quizFrame.quizState.setAnswer(answer); } }; QuestionPanel panel; if (question instanceof MultipleChoiceQuestion) { panel = new MultipleChoicePanel((MultipleChoiceQuestion) question, answerChangedObserver, quizFrame.quizState.shouldStateNumberOfOptionsNeededForMultipleChoice()); } else if (question instanceof DragAndDropQuestion) { panel = new DragAndDropPanel((DragAndDropQuestion) question, quizFrame, answerChangedObserver, quizFrame.glassPane); } else { throw new AssertionError("Unknown question type " + question.getClass()); } setPanel(questionNumber, panel, question); } } public static void main(String[] args) { InquisitionMain.setupLookAndFeel(); QuizConfig quizConfig = QuizConfig.createWithTimer(false, true, 30); // QuizConfig quizConfig = QuizConfig.createWithoutTimer(false, true); Collection<QuestionSet> bundledQuestionSets = QuestionSetManager.loadBundledQuestionSets(); Iterator<QuestionSet> iterator = bundledQuestionSets.iterator(); iterator.next(); QuestionSet questionSet = iterator.next(); QuizState quizState_ = new QuizState(questionSet, quizConfig); final QuizFrame quizFrame = new QuizFrame(quizState_, new ReturnCallback() { public void doReturn() { // Do nothing } }); SwingUtilities.invokeLater(new Runnable() { public void run() { quizFrame.setDefaultCloseOperation(EXIT_ON_CLOSE); quizFrame.setVisible(true); } }); } }