/* * 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 jmemorize.core.Card; import jmemorize.core.Category; import jmemorize.core.CategoryObserver; import jmemorize.core.Lesson; import jmemorize.core.Main; import jmemorize.core.Settings; import jmemorize.core.Main.ProgramEndObserver; import jmemorize.core.learn.LearnHistory; import jmemorize.core.learn.LearnSession; import jmemorize.core.learn.LearnSessionObserver; import jmemorize.core.learn.LearnHistory.SessionSummary; import jmemorize.gui.LC; import jmemorize.gui.Localization; import jmemorize.gui.swing.GeneralTransferHandler; 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.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 { 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"; // jmemorize swing elements private CategoryComboBox m_categoryBox; private CategoryTree m_categoryTree; private DeckTablePanel m_deckTablePanel; private DeckChartPanel m_deckChartPanel; private LearnPanel m_learnPanel; private StatusBar m_statusBar = new StatusBar(); private 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 Main m_main; private Category m_category; private int m_deck; private 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; // set look and feel before we load any frames static { try { // UIManager.setLookAndFeel(new MetalLookAndFeel()); UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (Exception e) { Main.logThrowable("could not set look and feel", e); } } /** * Creates a new MainFrame. */ public MainFrame() { 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. */ public void selectionChanged(SelectionProvider selectionProvider) { m_focusedCategories = null; updateSelectionObservers(); } /* (non-Javadoc) * @see jmemorize.gui.swing.SelectionProvider */ public List<Card> getRelatedCards() { return m_focusedCategories == null ? getCurrentSelectionProvider().getRelatedCards() : null; } /* (non-Javadoc) * @see jmemorize.gui.swing.SelectionProvider */ public List<Card> getSelectedCards() { return m_focusedCategories == null ? getCurrentSelectionProvider().getSelectedCards() : null; } /* (non-Javadoc) * @see jmemorize.gui.swing.SelectionProvider */ public List<Category> getSelectedCategories() { return m_focusedCategories; // can be null } /** * @return Returns the currently displayed category. */ public Category getCategory() { return m_category; } /* (non-Javadoc) * @see jmemorize.gui.swing.SelectionProvider */ public void addSelectionObserver(SelectionObserver observer) { m_selectionObservers.add(observer); } /* (non-Javadoc) * @see jmemorize.gui.swing.SelectionProvider */ public void removeSelectionObserver(SelectionObserver observer) { m_selectionObservers.remove(observer); } public void setLesson(Lesson lesson) { 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(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 */ public JComponent getDefaultFocusOwner() { return m_categoryTree.isFocusOwner() ? (JComponent)m_categoryTree : (JComponent)m_deckTablePanel.getCardTable(); } /* (non-Javadoc) * @see jmemorize.gui.swing.SelectionProvider */ 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(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(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(Category category, List<Card> selectedCards, boolean learnUnlearned, 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 */ public void onCategoryEvent(int type, Category category) { if (type == REMOVED_EVENT) { setCategory(m_main.getLesson().getRootCategory()); // HACK } } /* (non-Javadoc) * @see jmemorize.core.CategoryObserver */ public void onCardEvent(int type, Card card, Category category, int deck) { // ignore } /** * Loads the lesson and sets it as currently loaded lesson. If there is * already a opened lesson, the user might be prompted to save that lesson * before opening the new lesson. * * @param file The path to the lesson. If <code>null</code> a file chooser * is shown that allows the user to select the file. */ public void loadLesson(File file) { try { if (!confirmCloseLesson()) return; if (file == null) { JFileChooser chooser = new JFileChooser(); try { chooser.setCurrentDirectory(Settings.loadLastDirectory()); } catch (Exception ioe) { Main.logThrowable("Could not load last directory", ioe); chooser.setCurrentDirectory(null); } chooser.setFileFilter(MainFrame.FILE_FILTER); int returnVal = chooser.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { file = chooser.getSelectedFile(); } else { return; } } m_main.loadLesson(file); Settings.storeLastDirectory(file); LearnHistory history = m_main.getLesson().getLearnHistory(); if (!history.isLoaded()) importGlobalLearnHistory(history); } catch (Exception e) { Object[] args = {file != null ? file.getName() : "?"}; MessageFormat form = new MessageFormat(Localization.get(LC.ERROR_LOAD)); String msg = form.format(args); Main.logThrowable(msg, e); new ErrorDialog(this, msg, e).setVisible(true); } } /** * Saves the lesson or displays an error message if the operation failed. * * @param file The path to the lesson. If <code>null</code> a file chooser * is shown that allows the user to select the file. */ public void saveLesson(Lesson lesson, File file) { try { if (file == null) { file = AbstractExportAction.showSaveDialog( this, MainFrame.FILE_FILTER); if (file == null) return; } m_main.saveLesson(lesson, file); updateFrameTitle(); } catch (Exception e) { Object[] args = {file != null ? file.getName() : "?"}; MessageFormat form = new MessageFormat(Localization.get(LC.ERROR_SAVE)); String msg = form.format(args); Main.logThrowable(msg, e); new ErrorDialog(this, msg, e).setVisible(true); } } /** * 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 confirmCloseLesson() { // first check the editCardFrame for unsaved changes 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 Lesson lesson = m_main.getLesson(); if (lesson.canSave()) { int n = JOptionPane.showConfirmDialog(MainFrame.this, Localization.get("MainFrame.SAVE_MODIFIED"), "Warning", //$NON-NLS-1$ //$NON-NLS-2$ JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); if (n == JOptionPane.OK_OPTION) { saveLesson(lesson, lesson.getFile()); // 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 */ public void onProgramEnd() { int hSize = m_showTreeButton.isSelected() ? m_horizontalSplitPane.getDividerLocation() : m_categoryTreeWidth; Settings.storeCategoryTreeWidth(hSize); 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 */ public void sessionStarted(LearnSession session) { gotoLearnMode(); } /* (non-Javadoc) * @see jmemorize.core.Main.LearnSessionStartObserver */ public void sessionEnded(LearnSession session) { showSessionChart(session); gotoBrowseMode(); } /** * Displays a dialog which summarizes the given session outcome. */ private void showSessionChart(LearnSession session) { if (!session.isRelevant()) return; 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 (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() { String name = Main.PROPERTIES.getProperty("project.name"); //$NON-NLS-1$ String version = Main.PROPERTIES.getProperty("project.version"); //$NON-NLS-1$ String suffix = " - " + name + " " + version; //$NON-NLS-1$ //$NON-NLS-2$ File file = m_main.getLesson().getFile(); if (file != null && !getTitle().equals(file.getName())) { setTitle(file.getName() + suffix); } else if (file == null) { setTitle(Localization.get("MainFrame.UNNAMED_LESSON") + suffix); //$NON-NLS-1$ } } private SelectionProvider getCurrentSelectionProvider() { if (m_main.isSessionRunning()) { return m_learnPanel; } else { return m_deckTablePanel.getCardTable(); } } private void initComponents() { 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 JPanel northPanel = new JPanel(); northPanel.setLayout(new BoxLayout(northPanel, BoxLayout.Y_AXIS)); northPanel.add(buildOperationsBar()); northPanel.add(buildCategoryBar()); m_categoryTree = new CategoryTree(); m_categoryTree.addSelectionObserver(new SelectionObserver() { public void selectionChanged(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); BasicSplitPaneUI ui = (BasicSplitPaneUI)m_verticalSplitPane.getUI(); ui.getDivider().setBorder(new EmptyBorder(5, 2, 5, 2)); 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(4); 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())); setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent evt) { ExitAction.exit(); } }); setIconImage(Toolkit.getDefaultToolkit().getImage( getClass().getResource("/resource/icons/main.png"))); //$NON-NLS-1$ pack(); } private JPanel buildCategoryBar() { 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); 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() { public void actionPerformed(ActionEvent evt) { categoryBoxActionPerformed(); } }); categoryToolbar.add(m_categoryBox); categoryToolbar.add(new JButton(new SplitMainFrameAction(this))); JPanel categoryPanel = new JPanel(new BorderLayout()); categoryPanel.setBorder(new EtchedBorder()); categoryPanel.add(categoryToolbar, BorderLayout.NORTH); return categoryPanel; } private JPanel buildOperationsBar() { JToolBar operationsToolbar = new JToolBar(); operationsToolbar.setFloatable(false); operationsToolbar.add(new JButton(new NewLessonAction())); operationsToolbar.add(new JButton(new OpenLessonAction())); operationsToolbar.add(new JButton(new SaveLessonAction())); 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())); operationsToolbar.add(new JButton(new LearnAction(this))); 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(SelectionProvider source) { assert source == m_categoryTree; if (!m_categoryTree.isPendingSelection()) { 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(LearnHistory history) { LearnHistory globalHistory = m_main.getGlobalLearnHistory(); for (SessionSummary summary : globalHistory.getSummaries()) { history.addSummary(summary.getStart(), summary.getEnd(), (int)summary.getPassed(), (int)summary.getFailed(), (int)summary.getSkipped(), (int)summary.getRelearned()); } } }