package org.korsakow.ide.ui.components;
import java.awt.Point;
import java.awt.dnd.DropTarget;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventObject;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.BorderFactory;
import javax.swing.ListSelectionModel;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableCellEditor;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.RowMapper;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.korsakow.ide.ui.components.tree.FolderNode;
import org.korsakow.ide.ui.components.tree.KNode;
import org.korsakow.ide.ui.components.tree.KTreeTableModel;
import org.korsakow.ide.util.Platform;
import org.korsakow.ide.util.UIUtil;
import org.korsakow.ide.util.Util;
import com.sun.swingx.JTreeTable;
import com.sun.swingx.TreeTableCellEditor;
import com.sun.swingx.treetable.TreeTableModel;
import com.sun.swingx.treetable.TreeTableModelAdapter;
public class KTreeTable extends JTreeTable
{
protected boolean editable;
protected Boolean canCollpaseRoot = null;
protected FocusListener editorFocusListener = new FocusAdapter() {
@Override
public void focusLost(FocusEvent event) {
TableCellEditor editor = getCellEditor();
if (editor != null) {
editor.stopCellEditing();
}
}
};
/**
* Used to prevent the tree from altering the selection when forwarding MouseEvents to it. Having both the table and tree
* change the selection based on user input is problematic since their semantics differ and you end up with slightly
* different selection behavior depending on if the user for example clicked on the tree directly or on on table portion outside the tree.
*
*/
protected boolean ignoreTreeSelection = false;
public KTreeTable(TreeTableModel model)
{
super(model);
setColumnSelectionAllowed(false);
setDefaultEditor(TreeTableModel.class, new KTreeTableCellEditor(this));
getTree().setSelectionModel(new TreeSelectionModelWrapper(tree.getSelectionModel()));
getTree().getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
getTree().setBorder(BorderFactory.createLineBorder(UIManager.getColor("Table.gridColor"))); // fixes the tree-column's right border
addMouseListener(new StopEditingOnClickListener());
// getTree().setOpaque(false);
}
public KNode getSelectedNode()
{
List<? extends KNode> nodes = getSelectedNodes();
return nodes.isEmpty()?null:nodes.get(0);
}
public List<? extends KNode> getSelectedNodes()
{
int[] rows = getSelectedRows();
List<KNode> nodes = new ArrayList<KNode>();
for (int row : rows) {
KNode node = getNodeAt(row);
nodes.add(node);
}
return nodes;
}
@Override
public DefaultTableColumnModel getColumnModel()
{
return (DefaultTableColumnModel)super.getColumnModel();
}
public FolderNode getRootNode()
{
return (FolderNode)tree.getModel().getRoot();
}
@Override
public boolean isCellEditable(int row, int column)
{
// return false;
return editable && super.isCellEditable(row, column);
}
public void setEditable(boolean editable)
{
// tree.setEditable(editable);
this.editable = editable;
}
public void setRootVisible(boolean rootVisible)
{
tree.setRootVisible(rootVisible);
}
public void setTreeCellRenderer(TreeCellRenderer renderer)
{
tree.setCellRenderer(renderer);
}
public void setTreeTableModel(KTreeTableModel model)
{
// maintain columns
List<?> identifiers = getTreeTableModel().getColumnIdentifiers();
model.setColumnIdentifiers(identifiers);
getTree().setModel(model);
setModel(new TreeTableModelAdapter(model, getTree()));
model.addTreeModelListener(new TreeModelListener() {
public void treeNodesChanged(TreeModelEvent e) {
expandNode(getRootNode());
}
public void treeNodesInserted(TreeModelEvent e) {
expandNode(getRootNode());
}
public void treeNodesRemoved(TreeModelEvent e) {
expandNode(getRootNode());
}
public void treeStructureChanged(TreeModelEvent e) {
expandNode(getRootNode());
}
});
}
public KTreeTableModel getTreeTableModel()
{
return (KTreeTableModel)super.getTree().getModel();
}
public KNode getNodeForPath(List<String> namespath)
{
List<String> names = new ArrayList<String>(namespath);
if (names.isEmpty())
return null;
KNode node = getRootNode();
if (!names.remove(0).equals(node.getName()))
throw new IllegalArgumentException("named path must start at root");
while (!names.isEmpty() && node!=null) {
String name = names.remove(0);
node = node.getChild(name);
if (node == null) {
throw new IllegalArgumentException("invalid path: " + Util.join(namespath));
// return null;
}
}
return node;
}
public int getRow(KNode node)
{
// does treetable support a better way to do this?
for (int i = 0; i < getRowCount(); ++i)
if (getNodeAt(i) == node)
return i;
return -1;
}
public TreePath getPathForLocation(int x, int y)
{
return tree.getPathForLocation(x, y);
}
public TreePath getPathForLocation(Point p)
{
return getPathForLocation(p.x, p.y);
}
public TreePath getPathForRow(int row)
{
return tree.getPathForRow(row);
}
public KNode getNodeAt(int row)
{
TreePath path = tree.getPathForRow(row);
if (path == null)
return null;
return (KNode)path.getLastPathComponent();
}
public void setRootCollapsible(boolean b)
{
if (!b) {
expandNode(getRootNode()); // otherwise it'd be forever invisible
}
if (canCollpaseRoot == null) // lazy init
{
getTree().addTreeWillExpandListener(new TreeWillExpandListener() {
public void treeWillCollapse(TreeExpansionEvent event)
throws ExpandVetoException {
if (!canCollpaseRoot && event.getPath().getLastPathComponent() == getRootNode())
throw new ExpandVetoException(event);
}
public void treeWillExpand(TreeExpansionEvent event)
throws ExpandVetoException {
// if (!canExpandRoot && event.getPath().getLastPathComponent() == getRootNode())
// throw new ExpandVetoException(event);
}
});
}
canCollpaseRoot = b;
}
public void expandNode(KNode node)
{
if (node.isLeaf() && node != getRootNode())
node = node.getParent();
if (node != null)
tree.expandPath(node.getTreePath());
// expandRow(getRow(node));
}
public void expandNodes(Collection<KNode> nodes)
{
for (KNode node : nodes)
{
expandNode(node);
}
}
public void expandAllRecursive()
{
expandAllRecursive(getRootNode());
}
public void expandAllRecursive(KNode node)
{
if (node.isLeaf()) {
expandNode(node);
} else {
for (KNode child : node)
expandAllRecursive(child);
}
}
/**
* @return the set of all nodes which are expanded
*/
public Set<KNode> getExpandedNodes()
{
Set<KNode> nodes = new HashSet<KNode>();
getExpandedNodes(nodes, getRootNode());
return nodes;
}
private void getExpandedNodes(Set<KNode> nodes, KNode node)
{
if (!tree.isExpanded(node.getTreePath()))
return; // if we're not expanded neither are our children
nodes.add(node);
for (KNode child : node.getChildren()) {
getExpandedNodes(nodes, child);
}
}
public void editCellAt(KNode node, int col)
{
super.editCellAt(getRow(node), col);
}
@Override
public boolean editCellAt(int row, int column, EventObject e)
{
if (!isCellEditable(row, column))
return false;
if (e instanceof MouseEvent) {
MouseEvent me = (MouseEvent)e;
if (me.getClickCount() > 2)
return false;
}
boolean ret = super.editCellAt(row, column, e);
if (editorComp != null) { // this null check is without motivation
// this fixes the editor placement to overlap the original text properly
// i don't know where the specific numbers come from, they're empirical.
editorComp.removeFocusListener(editorFocusListener);
editorComp.addFocusListener(editorFocusListener);
}
return ret;
}
@Override
public Object getValueAt(int row, int col)
{
return super.getValueAt(row, col);
}
@Override
protected void processMouseEvent(MouseEvent event)
{
// L&F fix for MAC which uses CTL-left click as a popup trigger
if (Platform.isMacOS() && event.isControlDown() && event.getButton() == MouseEvent.BUTTON1) {
event = new MouseEvent(event.getComponent(), event.getID(), event.getWhen(), event.getModifiers(), event.getX(), event.getY(), event.getClickCount(), true);
}
// if (me.getID() == MouseEvent.MOUSE_CLICKED)
// if (me.getModifiers() == 0 ||
// me.getModifiers() == InputEvent.BUTTON1_MASK)
super.processMouseEvent(event);
{
for (int counter = getColumnCount() - 1; counter >= 0; counter--)
{
if (getColumnClass(counter) == TreeTableModel.class)
{
MouseEvent newME = new MouseEvent
(KTreeTable.this.getTree(), event.getID(),
event.getWhen(), event.getModifiers(),
event.getX() - getCellRect(0, counter, true).x,
event.getY(), event.getClickCount(),
UIUtil.isPopupTrigger(event));
// see comments for ignoreTreeSelection
ignoreTreeSelection = true;
KTreeTable.this.getTree().dispatchEvent(newME);
ignoreTreeSelection = false;
break;
}
}
}
}
/**
* fixes some wierd UI issues in JTreeTable.
* @author d
*
*/
public static class KTreeTableCellEditor extends TreeTableCellEditor
{
public KTreeTableCellEditor(JTreeTable jTreeTable) {
super(jTreeTable);
}
/**
* JTreeTable tries to use this opportunity to forward mouse events. We do so in processMouseEvents instead.
* Doing it here leads to wierd behaviors for example trying to expand/collapse nodes.
*
* (Strangely ?,) returning false here only prevents the mouse-driven double click event. Or at least
* we can (and still do) initiate editing programatically. This however is more or less what we want so its ok.
*/
@Override
public boolean isCellEditable(EventObject e) {
return e == null;
}
@Override
public KTreeTable getTreeTable() {
return (KTreeTable)super.getTreeTable();
}
}
/**
* A wrapper that optionally ignored attemps at selection @see ignoreTreeSelection
*/
public class TreeSelectionModelWrapper implements TreeSelectionModel
{
private final TreeSelectionModel delegate;
public TreeSelectionModelWrapper(TreeSelectionModel delegate)
{
this.delegate = delegate;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
delegate.addPropertyChangeListener(listener);
}
public void addSelectionPath(TreePath path) {
if (ignoreTreeSelection) return;
delegate.addSelectionPath(path);
}
public void addSelectionPaths(TreePath[] paths) {
if (ignoreTreeSelection) return;
delegate.addSelectionPaths(paths);
}
public void addTreeSelectionListener(TreeSelectionListener x) {
delegate.addTreeSelectionListener(x);
}
public void clearSelection() {
if (ignoreTreeSelection) return;
delegate.clearSelection();
}
public TreePath getLeadSelectionPath() {
return delegate.getLeadSelectionPath();
}
public int getLeadSelectionRow() {
return delegate.getLeadSelectionRow();
}
public int getMaxSelectionRow() {
return delegate.getMaxSelectionRow();
}
public int getMinSelectionRow() {
return delegate.getMinSelectionRow();
}
public RowMapper getRowMapper() {
return delegate.getRowMapper();
}
public int getSelectionCount() {
return delegate.getSelectionCount();
}
public int getSelectionMode() {
return delegate.getSelectionMode();
}
public TreePath getSelectionPath() {
return delegate.getSelectionPath();
}
public TreePath[] getSelectionPaths() {
return delegate.getSelectionPaths();
}
public int[] getSelectionRows() {
return delegate.getSelectionRows();
}
public boolean isPathSelected(TreePath path) {
return delegate.isPathSelected(path);
}
public boolean isRowSelected(int row) {
return delegate.isRowSelected(row);
}
public boolean isSelectionEmpty() {
return delegate.isSelectionEmpty();
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
delegate.removePropertyChangeListener(listener);
}
public void removeSelectionPath(TreePath path) {
if (ignoreTreeSelection) return;
delegate.removeSelectionPath(path);
}
public void removeSelectionPaths(TreePath[] paths) {
if (ignoreTreeSelection) return;
delegate.removeSelectionPaths(paths);
}
public void removeTreeSelectionListener(TreeSelectionListener x) {
delegate.removeTreeSelectionListener(x);
}
public void resetRowSelection() {
if (ignoreTreeSelection) return;
delegate.resetRowSelection();
}
public void setRowMapper(RowMapper newMapper) {
delegate.setRowMapper(newMapper);
}
public void setSelectionMode(int mode) {
if (ignoreTreeSelection) return;
delegate.setSelectionMode(mode);
}
public void setSelectionPath(TreePath path) {
if (ignoreTreeSelection) return;
delegate.setSelectionPath(path);
}
public void setSelectionPaths(TreePath[] paths) {
if (ignoreTreeSelection) return;
delegate.setSelectionPaths(paths);
}
}
/**
* Um... the default implementation will ignore our DropTarget if it implements UIResource
* yet BasicTableUI won't do the selection changing while dragging UNLESS it implements UIResource. arg.
* @param handler
* @param dropTarget
*/
public void setTransferHandler(TransferHandler handler, DropTarget dropTarget)
{
}
public class StopEditingOnClickListener extends MouseAdapter
{
@Override
public void mousePressed(MouseEvent mouseEvent)
{
// TableCellEditor editor = getCellEditor();
// if (editor != null) {
// editor.stopCellEditing();
// }
}
}
public MouseListener createStopEditingOnClickListener()
{
return new StopEditingOnClickListener();
}
@Override
public void editingStopped(ChangeEvent e) {
// prevents a host of NPE
if (getEditingRow() != -1)
super.editingStopped(e);
else {
TableCellEditor editor = getCellEditor();
if (editor != null) {
removeEditor();
}
}
}
}