Java tutorial
/* * jMemorize - Learning made easy (and fun) - A Leitner flashcards tool * Copyright(C) 2004-2008 Riad Djemili and contributors * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 1, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package jmemorize.gui.swing.frames; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.text.MessageFormat; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToolBar; import javax.swing.SwingConstants; import javax.swing.TransferHandler; import javax.swing.UIManager; import javax.swing.WindowConstants; import javax.swing.border.EmptyBorder; import javax.swing.border.EtchedBorder; import javax.swing.plaf.basic.BasicSplitPaneUI; import org.apache.commons.lang.StringUtils; import jmemorize.core.Card; import jmemorize.core.Category; import jmemorize.core.CategoryObserver; import jmemorize.core.LC; import jmemorize.core.Lesson; import jmemorize.core.Localization; import jmemorize.core.Model.ProgramEndObserver; import jmemorize.core.Settings; import jmemorize.core.io.JMemorizeIO; import jmemorize.core.io.JmlIO; import jmemorize.core.learn.LearnHistory; import jmemorize.core.learn.LearnHistory.SessionSummary; import jmemorize.core.learn.LearnSession; import jmemorize.core.learn.LearnSessionObserver; import jmemorize.gui.swing.GeneralTransferHandler; import jmemorize.gui.swing.Main; import jmemorize.gui.swing.MainMenu; import jmemorize.gui.swing.NewCardFramesManager; import jmemorize.gui.swing.SelectionProvider; import jmemorize.gui.swing.SelectionProvider.SelectionObserver; import jmemorize.gui.swing.actions.LearnAction; import jmemorize.gui.swing.actions.ShowCategoryTreeAction; import jmemorize.gui.swing.actions.SplitMainFrameAction; import jmemorize.gui.swing.actions.edit.AddCardAction; import jmemorize.gui.swing.actions.edit.AddCategoryAction; import jmemorize.gui.swing.actions.edit.EditCardAction; import jmemorize.gui.swing.actions.edit.FindAction; import jmemorize.gui.swing.actions.edit.RemoveAction; import jmemorize.gui.swing.actions.edit.ResetCardAction; import jmemorize.gui.swing.actions.file.AbstractExportAction; import jmemorize.gui.swing.actions.file.ExitAction; import jmemorize.gui.swing.actions.file.JMemorizeUI; import jmemorize.gui.swing.actions.file.NewLessonAction; import jmemorize.gui.swing.actions.file.OpenLessonAction; import jmemorize.gui.swing.actions.file.SaveLessonAction; import jmemorize.gui.swing.dialogs.ErrorDialog; import jmemorize.gui.swing.dialogs.OkayButtonDialog; import jmemorize.gui.swing.panels.DeckChartPanel; import jmemorize.gui.swing.panels.DeckTablePanel; import jmemorize.gui.swing.panels.LearnPanel; import jmemorize.gui.swing.panels.SessionChartPanel; import jmemorize.gui.swing.panels.StatusBar; import jmemorize.gui.swing.widgets.CategoryComboBox; import jmemorize.gui.swing.widgets.CategoryTree; import jmemorize.util.ExtensionFileFilter; /** * The main window of jMemorize. It has a stats panel in the upper part and a * card table/learn panel in the bottom. Optionaly there is also a category tree * at the left side. * * @author djemili */ public class MainFrame extends JFrame implements CategoryObserver, SelectionProvider, SelectionObserver, LearnSessionObserver, ProgramEndObserver, JMemorizeUI { /** * */ private static final long serialVersionUID = 2885593013862395823L; static public final TransferHandler TRANSFER_HANDLER = new GeneralTransferHandler(); public static final ExtensionFileFilter FILE_FILTER = new ExtensionFileFilter("jml", Localization.get(LC.FILE_FILTER_DESC)); private static final String FRAME_ID = "main"; private static final String REPEAT_CARD = "repeatCard"; private static final String DECK_CARD = "deckCard"; public static final int DIVIDER_SIZE = 4; // jmemorize swing elements private CategoryComboBox m_categoryBox; private CategoryTree m_categoryTree; private DeckTablePanel m_deckTablePanel; private DeckChartPanel m_deckChartPanel; private LearnPanel m_learnPanel; private final StatusBar m_statusBar = new StatusBar(); private final NewCardFramesManager m_newCardManager = new NewCardFramesManager(); // native swing elements private JPanel m_bottomPanel; private JButton m_showTreeButton; private JSplitPane m_horizontalSplitPane; private JSplitPane m_verticalSplitPane; private JScrollPane m_treeScrollPane; private final Main m_main; private Category m_category; private int m_deck; private final List<SelectionObserver> m_selectionObservers = new LinkedList<SelectionObserver>(); // category tree private boolean m_showCategoryTree; private boolean m_showCategoryTreeOld; private int m_categoryTreeWidth = Settings.loadCategoryTreeWidth(); // either cards or categories can be focused, not both at the same time // UGLYHACK remove private List<Category> m_focusedCategories; private final JMemorizeIO jMemorizeIO; // set look and feel before we load any frames static { try { // UIManager.setLookAndFeel(new MetalLookAndFeel()); UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (final Exception e) { Main.logThrowable("could not set look and feel", e); } } public static void beautifyDividerBorder(final JSplitPane splitPane) { final BasicSplitPaneUI ui = (BasicSplitPaneUI) splitPane.getUI(); ui.getDivider().setBorder(new EmptyBorder(5, 2, 5, 2)); } /** * Creates a new MainFrame. */ public MainFrame() { jMemorizeIO = new JmlIO(this); m_main = Main.getInstance(); initComponents(); loadSettings(); m_deckTablePanel.getCardTable().setStatusBar(m_statusBar); m_learnPanel.setStatusBar(m_statusBar); setLesson(m_main.getLesson()); // GUI is first loaded with empty lesson gotoBrowseMode(); m_main.addLearnSessionObserver(this); m_main.addProgramEndObserver(this); } /* * Simply listen for card selection events in learn and decktable panel and * forward them. */ @Override public void selectionChanged(final SelectionProvider selectionProvider) { m_focusedCategories = null; updateSelectionObservers(); } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public List<Card> getRelatedCards() { return m_focusedCategories == null ? getCurrentSelectionProvider().getRelatedCards() : null; } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public List<Card> getSelectedCards() { return m_focusedCategories == null ? getCurrentSelectionProvider().getSelectedCards() : null; } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public List<Category> getSelectedCategories() { return m_focusedCategories; // can be null } /** * @return Returns the currently displayed category. */ @Override public Category getCategory() { return m_category; } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public void addSelectionObserver(final SelectionObserver observer) { m_selectionObservers.add(observer); } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public void removeSelectionObserver(final SelectionObserver observer) { m_selectionObservers.remove(observer); } public void setLesson(final Lesson lesson) { final Category rootCategory = lesson.getRootCategory(); m_categoryBox.setRootCategory(rootCategory); m_categoryTree.setRootCategory(rootCategory); setCategory(rootCategory); EditCardFrame.getInstance().setVisible(false); FindFrame.getInstance().setVisible(false); updateFrameTitle(); } public void setCategory(final Category category) { if (category == null) // HACK return; if (m_category != null) { m_category.removeObserver(this); } m_category = category; m_category.addObserver(this); m_deckChartPanel.setCategory(category); m_deckTablePanel.setCategory(category); // TODO refactor. give only list // of cards m_categoryBox.setSelectedCategory(category); m_categoryTree.setSelectedCategory(category); // in learn mode the focused item should always our currently learned // card if (!Main.getInstance().isSessionRunning()) // HACK { m_focusedCategories = new ArrayList<Category>(1); // HACK m_focusedCategories.add(category); } updateSelectionObservers(); } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public JComponent getDefaultFocusOwner() { return m_categoryTree.isFocusOwner() ? (JComponent) m_categoryTree : (JComponent) m_deckTablePanel.getCardTable(); } /* * (non-Javadoc) * * @see jmemorize.gui.swing.SelectionProvider */ @Override public JFrame getFrame() { return this; } /** * Set the currently displayed deck. * * @param level * the level of the deck that is to be shown. The deck with * unlearned cards has level 0. */ public void setDeck(final int level) { m_deck = level; m_deckTablePanel.setDeck(level); m_deckChartPanel.setDeck(level); } /** * @return the level of the currently shown deck. The deck with unlearned * cards has level 0. */ public int getDeck() { return m_deck; } /** * @param show * <code>true</code> if the category tree is supposed to be * shown. <code>false</code> otherwise. */ public void showCategoryTree(final boolean show) { if (!show) { if (m_showCategoryTree) { m_categoryTreeWidth = m_horizontalSplitPane.getDividerLocation(); } m_horizontalSplitPane.setDividerSize(0); m_showTreeButton.setSelected(false); m_treeScrollPane.setVisible(false); } else { if (!m_showCategoryTree) { m_horizontalSplitPane.setDividerLocation(m_categoryTreeWidth); } m_showTreeButton.setSelected(true); m_treeScrollPane.setVisible(true); m_horizontalSplitPane.setDividerSize(5); } m_showCategoryTree = show; } /** * @return <code>true</code> if the category tree is currently visible. */ public boolean isShowCategoryTree() { return m_showCategoryTree; } public void startLearning(final Category category, final List<Card> selectedCards, final boolean learnUnlearned, final boolean learnExpired) { m_showCategoryTreeOld = m_showCategoryTree; showCategoryTree(false); m_main.startLearnSession(m_main.getLearnSettings(), selectedCards, category, learnUnlearned, learnExpired); } public NewCardFramesManager getNewCardManager() // TODO pull up to a new // common singleton { return m_newCardManager; } public LearnPanel getLearnPanel() { return m_learnPanel; } public JSplitPane getVerticalSplitPane() { return m_verticalSplitPane; } /* * (non-Javadoc) * * @see jmemorize.core.CategoryObserver */ @Override public void onCategoryEvent(final int type, final Category category) { if (type == REMOVED_EVENT) { setCategory(m_main.getLesson().getRootCategory()); // HACK } } /* * (non-Javadoc) * * @see jmemorize.core.CategoryObserver */ @Override public void onCardEvent(final int type, final Card card, final Category category, final int deck) { // ignore } /** * If lesson was modified this shows a dialog that asks if the user wants to * save the lesson before closing it. * * @return <code>true</code> if user chose not to cancel the lesson close * operation. If this method return <code>false</code> the closing * of jMemorize was canceled. */ public boolean allowTheUserToSaveIfClosing() { // first check the editCardFrame for unsaved changes final EditCardFrame editFrame = EditCardFrame.getInstance(); if (editFrame.isVisible() && !editFrame.close()) { return false; // user canceled closing of edit card frame } if (!m_newCardManager.closeAllFrames()) // close all addCard frames { return false; } // then see if lesson should to be saved final Lesson lesson = m_main.getLesson(); if (lesson.canSave()) { final int n = JOptionPane.showConfirmDialog(MainFrame.this, Localization.get("MainFrame.SAVE_MODIFIED"), //$NON-NLS-1$ "Warning", //$NON-NLS-1$ JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); if (n == JOptionPane.OK_OPTION) { try { jMemorizeIO.saveLesson(lesson); jMemorizeIO.reset(); } catch (final Exception exception) { final File file = jMemorizeIO.getFile(); final Object[] args = { file != null ? file.getName() : "?" }; final MessageFormat form = new MessageFormat(Localization.get(LC.ERROR_SAVE)); final String msg = form.format(args); Main.logThrowable(msg, exception); new ErrorDialog(this, msg, exception).setVisible(true); } // if lesson was saved return true, false otherwise return !lesson.canSave(); } // if NO chosen continue, otherwise CANCEL was chosen return n == JOptionPane.NO_OPTION; } return true; } /* * (non-Javadoc) * * @see jmemorize.core.Main.ProgramEndObserver */ @Override public void onProgramEnd() { final int hSize = m_showTreeButton.isSelected() ? m_horizontalSplitPane.getDividerLocation() : m_categoryTreeWidth; Settings.storeCategoryTreeWidth(hSize); final int vSize = m_verticalSplitPane.getDividerLocation() > 0 ? m_verticalSplitPane.getDividerLocation() : m_verticalSplitPane.getLastDividerLocation(); Settings.storeMainDividerLocation(vSize); Settings.storeCategoryTreeVisible(m_showTreeButton.isSelected()); Settings.storeFrameState(this, FRAME_ID); } /* * (non-Javadoc) * * @see jmemorize.core.Main.LearnSessionStartObserver */ @Override public void sessionStarted(final LearnSession session) { gotoLearnMode(); } /* * (non-Javadoc) * * @see jmemorize.core.Main.LearnSessionStartObserver */ @Override public void sessionEnded(final LearnSession session) { showSessionChart(session); gotoBrowseMode(); } /** * Displays a dialog which summarizes the given session outcome. */ private void showSessionChart(final LearnSession session) { if (!session.isRelevant()) return; final JDialog dialog = new OkayButtonDialog(this, Localization.get("Learn.SESSION_RESULTS"), //$NON-NLS-1$ true, new SessionChartPanel(session)); dialog.setLocationRelativeTo(this); dialog.setVisible(true); } private void gotoBrowseMode() { m_learnPanel.removeSelectionObserver(this); m_deckTablePanel.getCardTable().addSelectionObserver(this); ((CardLayout) m_bottomPanel.getLayout()).show(m_bottomPanel, DECK_CARD); m_focusedCategories = null; showCategoryTree(m_showCategoryTreeOld); updateSelectionObservers(); } private void gotoLearnMode() { m_deckTablePanel.getCardTable().removeSelectionObserver(this); m_learnPanel.addSelectionObserver(this); ((CardLayout) m_bottomPanel.getLayout()).show(m_bottomPanel, REPEAT_CARD); m_focusedCategories = null; setDeck(-1); // needed to get right values in status bar while learning updateSelectionObservers(); } private void updateSelectionObservers() { for (final SelectionObserver listener : m_selectionObservers) listener.selectionChanged(this); } /** * Update the frame title. This should be called when a new lesson was * loaded or changed. */ private void updateFrameTitle() { final String name = Main.PROPERTIES.getProperty("project.name"); //$NON-NLS-1$ final String version = Main.PROPERTIES.getProperty("project.version"); //$NON-NLS-1$ final String suffix = " - " + name + " " + version; //$NON-NLS-1$ //$NON-NLS-2$ final String title = m_main.getLesson().getTitle(); if (StringUtils.isEmpty(title)) { setTitle(Localization.get("MainFrame.UNNAMED_LESSON") + suffix); //$NON-NLS-1$ } else { setTitle(title); } } private SelectionProvider getCurrentSelectionProvider() { if (m_main.isSessionRunning()) { return m_learnPanel; } else { return m_deckTablePanel.getCardTable(); } } private void initComponents() { final JPanel mainPanel = new JPanel(new BorderLayout()); m_deckChartPanel = new DeckChartPanel(this); m_deckChartPanel.setMinimumSize(new Dimension(100, 150)); m_learnPanel = new LearnPanel(); m_deckTablePanel = new DeckTablePanel(this); // north panel final JPanel northPanel = new JPanel(); northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.Y_AXIS)); northPanel.add(buildOperationsBar()); northPanel.add(buildCategoryBar()); m_categoryTree = new CategoryTree(jMemorizeIO); m_categoryTree.addSelectionObserver(new SelectionObserver() { @Override public void selectionChanged(final SelectionProvider source) { treeSelectionChanged(source); } }); m_treeScrollPane = new JScrollPane(m_categoryTree); // bottom panel m_bottomPanel = new JPanel(new CardLayout()); m_bottomPanel.add(m_deckTablePanel, DECK_CARD); m_bottomPanel.add(m_learnPanel, REPEAT_CARD); // vertical split pane m_verticalSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); m_verticalSplitPane.setPreferredSize(new Dimension(16, 500)); m_verticalSplitPane.setBorder(null); beautifyDividerBorder(m_verticalSplitPane); m_verticalSplitPane.setTopComponent(m_deckChartPanel); m_verticalSplitPane.setBottomComponent(m_bottomPanel); mainPanel.setPreferredSize(new Dimension(800, 500)); mainPanel.add(m_verticalSplitPane, BorderLayout.CENTER); // horizontal split pane m_horizontalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); m_horizontalSplitPane.setDividerLocation(m_categoryTreeWidth); m_horizontalSplitPane.setDividerSize(DIVIDER_SIZE); m_horizontalSplitPane.setBorder(null); m_horizontalSplitPane.setLeftComponent(m_treeScrollPane); m_horizontalSplitPane.setRightComponent(mainPanel); // frame content pane getContentPane().add(northPanel, BorderLayout.NORTH); getContentPane().add(m_horizontalSplitPane, BorderLayout.CENTER); getContentPane().add(m_statusBar, BorderLayout.SOUTH); setJMenuBar(new MainMenu(this, m_main.getRecentLessonFiles(), jMemorizeIO, this)); setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { @Override public void windowClosing(final WindowEvent evt) { ExitAction.exit(); } }); setIconImage(Toolkit.getDefaultToolkit().getImage(getClass().getResource("/resource/icons/main.png"))); //$NON-NLS-1$ pack(); } private JPanel buildCategoryBar() { final JToolBar categoryToolbar = new JToolBar(); categoryToolbar.setFloatable(false); categoryToolbar.setMargin(new Insets(2, 2, 2, 2)); m_showTreeButton = new JButton(new ShowCategoryTreeAction()); m_showTreeButton.setPreferredSize(new Dimension(120, 21)); categoryToolbar.add(m_showTreeButton); final JLabel categoryLabel = new JLabel(Localization.get(LC.CATEGORY), SwingConstants.CENTER); categoryLabel.setPreferredSize(new Dimension(60, 15)); categoryToolbar.add(categoryLabel); m_categoryBox = new CategoryComboBox(); m_categoryBox.setPreferredSize(new Dimension(24, 24)); m_categoryBox.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent evt) { categoryBoxActionPerformed(); } }); categoryToolbar.add(m_categoryBox); categoryToolbar.add(new JButton(new SplitMainFrameAction(this))); final JPanel categoryPanel = new JPanel(new BorderLayout()); categoryPanel.setBorder(new EtchedBorder()); categoryPanel.add(categoryToolbar, BorderLayout.NORTH); return categoryPanel; } private JPanel buildOperationsBar() { final JToolBar operationsToolbar = new JToolBar(); operationsToolbar.setFloatable(false); operationsToolbar.add(new JButton(new NewLessonAction())); operationsToolbar.add(new JButton(new OpenLessonAction(jMemorizeIO, this))); operationsToolbar.add(new JButton(new SaveLessonAction(jMemorizeIO))); operationsToolbar.add(new JButton(new AddCardAction(this))); operationsToolbar.add(new JButton(new EditCardAction(this))); operationsToolbar.add(new JButton(new ResetCardAction(this))); operationsToolbar.add(new JButton(new RemoveAction(this))); operationsToolbar.add(new JButton(new AddCategoryAction(this))); operationsToolbar.add(new JButton(new FindAction(this))); operationsToolbar.add(new JButton(new LearnAction(this))); final JPanel operationsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 1, 1)); operationsPanel.add(operationsToolbar); return operationsPanel; } private void categoryBoxActionPerformed() { setCategory(m_categoryBox.getSelectedCategory()); } private void treeSelectionChanged(final SelectionProvider source) { assert source == m_categoryTree; if (!m_categoryTree.isPendingSelection()) { final Category category = m_categoryTree.getSelectedCategory(); if (category != m_category) setCategory(category); } } private void loadSettings() { showCategoryTree(Settings.loadCategoryTreeVisible()); m_verticalSplitPane.setDividerLocation(Settings.loadMainDividerLocation()); Settings.loadFrameState(this, FRAME_ID); } private void importGlobalLearnHistory(final LearnHistory history) { final LearnHistory globalHistory = m_main.getGlobalLearnHistory(); for (final SessionSummary summary : globalHistory.getSummaries()) { history.addSummary(summary.getStart(), summary.getEnd(), (int) summary.getPassed(), (int) summary.getFailed(), (int) summary.getSkipped(), (int) summary.getRelearned()); } } @Override public File determineLessonFileToOpen() { // from determineLessonFile(file): final File lessonFile; final JFileChooser chooser = new JFileChooser(); try { chooser.setCurrentDirectory(Settings.loadLastDirectory()); } catch (final Exception ioe) { Main.logThrowable("Could not load last directory", ioe); chooser.setCurrentDirectory(null); } chooser.setFileFilter(MainFrame.FILE_FILTER); final int returnVal = chooser.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { lessonFile = chooser.getSelectedFile(); } else { lessonFile = null; } return lessonFile; } @Override public void show(final Lesson lesson) { setLesson(lesson); } @Override public File determineLessonFileToSave() { return AbstractExportAction.showSaveDialog(this, MainFrame.FILE_FILTER); } }