/*
* 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.widgets;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.EventObject;
import java.util.List;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import jmemorize.core.Card;
import jmemorize.core.Category;
import jmemorize.core.CategoryObserver;
import jmemorize.core.Main;
import jmemorize.gui.swing.SelectionProvider;
import jmemorize.gui.swing.actions.LearnAction;
import jmemorize.gui.swing.actions.edit.AddCardAction;
import jmemorize.gui.swing.actions.edit.AddCategoryAction;
import jmemorize.gui.swing.actions.edit.CopyAction;
import jmemorize.gui.swing.actions.edit.CutAction;
import jmemorize.gui.swing.actions.edit.PasteAction;
import jmemorize.gui.swing.actions.edit.RemoveAction;
import jmemorize.gui.swing.frames.MainFrame;
/**
* A category tree that shows a visual representation of the categories. It also
* has support for drag'n'drop actions and renaming categories.
*
* @author djemili
*/
public class CategoryTree extends JTree implements CategoryObserver, SelectionProvider
{
private class CellRenderer extends DefaultTreeCellRenderer
{
public CellRenderer()
{
setLeafIcon(FOLDER_ICON);
setOpenIcon(FOLDER_ICON);
setClosedIcon(FOLDER_ICON);
}
/**
* @see javax.swing.tree.DefaultTreeCellRenderer#getTreeCellRendererComponent
*/
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus)
{
JLabel label = (JLabel)super.getTreeCellRendererComponent(tree, value,
sel, expanded, leaf, row, hasFocus);
Object nodeValue = ((DefaultMutableTreeNode)value).getUserObject();
if (nodeValue instanceof Category)
{
Category category = (Category)nodeValue;
label.setText(category.getName());
}
return label;
}
}
private class CategoryTreeModel extends DefaultTreeModel
{
/**
* @param root
*/
public CategoryTreeModel(TreeNode root)
{
super(root);
}
/**
* Instead of overwriting userObject like the super method does, the
* input is set as the new name of the category.
*
* @see javax.swing.tree.DefaultTreeModel#valueForPathChanged
*/
public void valueForPathChanged(TreePath path, Object newValue)
{
DefaultMutableTreeNode aNode = (DefaultMutableTreeNode)path.getLastPathComponent();
Category category = (Category)aNode.getUserObject();
category.setName((String)newValue);
nodeChanged(aNode);
updateSelectionObservers();
}
}
private class CellEditor extends DefaultTreeCellEditor
{
private Category m_editedCategory;
private DefaultMutableTreeNode m_editedNode; //HACK
public CellEditor(JTree tree, DefaultTreeCellRenderer renderer)
{
super(tree, renderer);
}
public DefaultMutableTreeNode getEditedNode()
{
return m_editedNode;
}
/**
* @return Returns the nodeCategory.
*/
public Category getNodeCategory()
{
return m_editedCategory;
}
/**
* @see javax.swing.tree.DefaultTreeCellEditor#isCellEditable
*/
public boolean isCellEditable(EventObject event)
{
// event is null if edit is started by click-pause-click
if (event != null)
{
MouseEvent mEvent = (MouseEvent)event;
TreePath path = getPathForLocation(mEvent.getX(), mEvent.getY());
if (path != null)
{
m_editedNode = (DefaultMutableTreeNode)path.getLastPathComponent();
m_editedCategory = (Category)m_editedNode.getUserObject();
}
}
// make root not editable
return super.isCellEditable(event) && m_editedCategory != m_rootCategory;
}
/**
* @see javax.swing.tree.DefaultTreeCellEditor#getTreeCellEditorComponent
*/
public Component getTreeCellEditorComponent(JTree tree, Object value,
boolean isSelected, boolean expanded, boolean leaf, int row)
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode)value;
Category category = (Category)node.getUserObject();
return super.getTreeCellEditorComponent(tree, category.getName(),
isSelected, expanded, leaf, row);
}
}
/**
* Is responsible for setting the previous category after a action has happened.
*/
private class ActionWrapper implements Action
{
private Action m_wrapped;
public ActionWrapper(Action original)
{
m_wrapped = original;
}
public void addPropertyChangeListener(PropertyChangeListener listener)
{
m_wrapped.addPropertyChangeListener(listener);
}
public Object getValue(String key)
{
return m_wrapped.getValue(key);
}
public boolean isEnabled()
{
return m_wrapped.isEnabled();
}
public void putValue(String key, Object value)
{
m_wrapped.putValue(key, value);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
m_wrapped.removePropertyChangeListener(listener);
}
public void setEnabled(boolean b)
{
m_wrapped.setEnabled(b);
}
public void actionPerformed(ActionEvent e)
{
m_wrapped.actionPerformed(e);
setSelectedCategory(m_beforeMenuCategory);
}
}
private final ImageIcon FOLDER_ICON = new ImageIcon(
getClass().getResource("/resource/icons/folder.gif")); //$NON-NLS-1$
private Category m_rootCategory;
/** The category that was shown before the popup menu was opened. */
private Category m_beforeMenuCategory;
private List<SelectionObserver> m_selectionObservers = new ArrayList<SelectionObserver>();
private JPopupMenu m_categoryMenu;
private boolean m_reopeningCategoryMenu = false;
public CategoryTree()
{
CellRenderer renderer = new CellRenderer();
setCellRenderer(renderer);
m_categoryMenu = buildCategoryContextMenu();
hookCategoryContextMenu();
setCellEditor(new CellEditor(this, renderer));
setTransferHandler(MainFrame.TRANSFER_HANDLER);
// addTreeSelectionListener(new TreeSelectionListener()
// {
// public void valueChanged(TreeSelectionEvent evt)
// {
// updateSelectionObservers();
// }
// });
setDragEnabled(true);
setEditable(true);
}
public void setRootCategory(Category category)
{
if (m_rootCategory != null)
{
m_rootCategory.removeObserver(this);
}
m_rootCategory = category;
m_rootCategory.addObserver(this);
MutableTreeNode root = createCategoryNode(category);
setModel(new CategoryTreeModel(root));
this.repaint();
setSelectedCategory(m_rootCategory);
}
public void setSelectedCategory(Category category)
{
if (category == null || m_rootCategory == null) //HACK
{
return;
}
DefaultMutableTreeNode root = (DefaultMutableTreeNode)getModel().getRoot();
Enumeration enumer = root.depthFirstEnumeration();
while (enumer.hasMoreElements())
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode)enumer.nextElement();
Object userValue = node.getUserObject();
if (userValue == category)
{
setSelectionPath(new TreePath(node.getPath()));
}
}
updateSelectionObservers();
}
public Category getSelectedCategory() // TODO replace by getCategory
{
return getCategory();
}
/**
* @return <code>true</code> if the selected category is pending. This is
* the case if the category was selected by right-clicking on the node.
*/
public boolean isPendingSelection()
{
return m_beforeMenuCategory != null;
}
/*
* @see jmemorize.core.CategoryObserver#onCategoryEvent
*/
public void onCategoryEvent(int type, Category category)
{
MutableTreeNode parent = null;
switch (type)
{
case ADDED_EVENT:
parent = getNode(category.getParent());
MutableTreeNode newChild = createCategoryNode(category);
int idx = category.getParent().getChildCategories().indexOf(category);
parent.insert(newChild, idx);
break;
case REMOVED_EVENT:
parent = getNode(category.getParent());
MutableTreeNode child = getNode(category);
parent.remove(child);
break;
case EDITED_EVENT:
parent = getNode(category);
break;
}
DefaultTreeModel model = (DefaultTreeModel)getModel();
model.reload(parent);
}
/*
* @see jmemorize.core.CategoryObserver#onCardEvent
*/
public void onCardEvent(int type, Card card, Category category, int deck)
{
// ignore
}
/* (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);
}
/* (non-Javadoc)
* @see jmemorize.gui.swing.SelectionProvider
*/
public Category getCategory()
{
TreePath path = getSelectionPath();
if (path != null)
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
return (Category)node.getUserObject();
}
return null;
}
/* (non-Javadoc)
* @see jmemorize.gui.swing.SelectionProvider
*/
public JComponent getDefaultFocusOwner()
{
return this;
}
/* (non-Javadoc)
* @see jmemorize.gui.swing.SelectionProvider
*/
public JFrame getFrame()
{
return Main.getInstance().getFrame();
}
/* (non-Javadoc)
* @see jmemorize.gui.swing.SelectionProvider
*/
public List<Card> getRelatedCards()
{
return null;
}
/* (non-Javadoc)
* @see jmemorize.gui.swing.SelectionProvider
*/
public List<Card> getSelectedCards()
{
return null;
}
/* (non-Javadoc)
* @see jmemorize.gui.swing.SelectionProvider
*/
public List<Category> getSelectedCategories()
{
List<Category> categories = new ArrayList<Category>();
categories.add(getCategory());
return categories;
}
private void updateSelectionObservers()
{
for (SelectionObserver observer : m_selectionObservers)
{
observer.selectionChanged(this);
}
}
private void hookCategoryContextMenu()
{
addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e)
{
updateSelectionObservers();
}
});
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e)
{
if (SwingUtilities.isLeftMouseButton(e))
{
int row = getRowForLocation(e.getX(), e.getY());
if (row > -1)
{
m_beforeMenuCategory = null;
}
m_reopeningCategoryMenu = false;
}
if (SwingUtilities.isRightMouseButton(e))
{
int row = getRowForLocation(e.getX(), e.getY());
if (row > -1)
{
m_reopeningCategoryMenu = true;
}
}
}
public void mouseClicked(MouseEvent e)
{
if (SwingUtilities.isRightMouseButton(e))
{
int row = getRowForLocation(e.getX(), e.getY());
if (row > -1)
{
if (!selectionModel.isRowSelected(row))
{
if (m_beforeMenuCategory == null)
m_beforeMenuCategory = getCategory();
setSelectionRow(row);
}
m_categoryMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
});
}
private JPopupMenu buildCategoryContextMenu()
{
JPopupMenu menu = new JPopupMenu();
menu.add(new ActionWrapper(new LearnAction(this)));
menu.add(new ActionWrapper(new AddCardAction(this)));
menu.add(new ActionWrapper(new AddCategoryAction(this)));
menu.add(new ActionWrapper(new RemoveAction(this)));
menu.addSeparator();
menu.add(new ActionWrapper(new CopyAction(this)));
menu.add(new ActionWrapper(new CutAction(this)));
menu.add(new ActionWrapper(new PasteAction(this)));
menu.addPopupMenuListener(new PopupMenuListener(){
public void popupMenuCanceled(PopupMenuEvent e)
{
// if (!m_reopeningCategoryMenu)
{
setSelectedCategory(m_beforeMenuCategory);
}
m_reopeningCategoryMenu = false;
}
public void popupMenuWillBecomeInvisible(PopupMenuEvent e)
{
// ignore
}
public void popupMenuWillBecomeVisible(PopupMenuEvent e)
{
// ignore
}
});
return menu;
}
private DefaultMutableTreeNode getNode(Object userValue)
{
DefaultTreeModel model = (DefaultTreeModel)getModel();
DefaultMutableTreeNode root = (DefaultMutableTreeNode)model.getRoot();
for (Enumeration enumer = root.depthFirstEnumeration(); enumer.hasMoreElements();)
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode)enumer.nextElement();
if (node.getUserObject() == userValue)
{
return node;
}
}
return null;
}
private MutableTreeNode createCategoryNode(Category category)
{
DefaultMutableTreeNode node = new DefaultMutableTreeNode(category);
// for all child categories
for (Category cat : category.getChildCategories())
{
node.add(createCategoryNode(cat));
}
return node;
}
}