blob: cfedc7274a91e2f259e6231cfb658b23a8a3ebf3 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ui.treeStructure.treetable;
import com.intellij.ui.table.JBTable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.EventObject;
import java.util.HashSet;
import java.util.Set;
/**
* This example shows how to create a simple JTreeTable component,
* by using a JTree as a renderer (and editor) for the cells in a
* particular column in the JTable.
*
* @version 1.2 10/27/98
*
* @author Philip Milne
* @author Scott Violet
*/
public class TreeTable extends JBTable {
/** A subclass of JTree. */
private TreeTableTree myTree;
private TreeTableModel myTableModel;
private PropertyChangeListener myTreeRowHeightPropertyListener;
public TreeTable(TreeTableModel treeTableModel) {
super();
setModel(treeTableModel);
}
@SuppressWarnings({"MethodOverloadsMethodOfSuperclass"})
public void setModel(TreeTableModel treeTableModel) {// Create the tree. It will be used as a renderer and editor.
if (myTree != null) {
myTree.removePropertyChangeListener(JTree.ROW_HEIGHT_PROPERTY, myTreeRowHeightPropertyListener);
}
myTree = new TreeTableTree(treeTableModel, this);
setRowHeight(myTree.getRowHeight());
myTreeRowHeightPropertyListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
int treeRowHeight = myTree.getRowHeight();
if (treeRowHeight == getRowHeight()) return;
setRowHeight(treeRowHeight);
}
};
myTree.addPropertyChangeListener(JTree.ROW_HEIGHT_PROPERTY, myTreeRowHeightPropertyListener);
// Install a tableModel representing the visible rows in the tree.
setTableModel(treeTableModel);
// Force the JTable and JTree to share their row selection models.
ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
myTree.setSelectionModel(selectionWrapper);
setSelectionModel(selectionWrapper.getListSelectionModel());
// Install the tree editor renderer and editor.
TreeTableCellRenderer treeTableCellRenderer = createTableRenderer(treeTableModel);
setDefaultRenderer(TreeTableModel.class, treeTableCellRenderer);
setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor(treeTableCellRenderer));
// No grid.
setShowGrid(false);
// No intercell spacing
setIntercellSpacing(new Dimension(0, 0));
// And update the height of the trees row to match that of the table.
if (myTree.getRowHeight() < 1) {
setRowHeight(18); // Metal looks better like this.
}
else {
setRowHeight(getRowHeight());
}
}
public TreeTableModel getTableModel() {
return myTableModel;
}
public void setTableModel(TreeTableModel treeTableModel) {
myTableModel = treeTableModel;
super.setModel(adapt(treeTableModel));
}
protected TreeTableModelAdapter adapt(TreeTableModel treeTableModel) {
return new TreeTableModelAdapter(treeTableModel, myTree, this);
}
public void setRootVisible(boolean visible){
myTree.setRootVisible(visible);
}
public void putTreeClientProperty(Object key, Object value){
myTree.putClientProperty(key, value);
}
public void setTreeCellRenderer(TreeCellRenderer renderer){
myTree.setCellRenderer(renderer);
}
/**
* Overridden to message super and forward the method to the tree.
* Since the tree is not actually in the component hierarchy it will
* never receive this unless we forward it in this manner.
*/
public void updateUI() {
super.updateUI();
if (myTree!= null) {
myTree.updateUI();
}
// Use the tree's default foreground and background colors in the
// table.
//noinspection HardCodedStringLiteral
LookAndFeel.installColorsAndFont(this, "Tree.background", "Tree.foreground", "Tree.font");
}
/* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
* paint the editor. The UI currently uses different techniques to
* paint the renderers and editors and overriding setBounds() below
* is not the right thing to do for an editor. Returning -1 for the
* editing row in this case, ensures the editor is never painted.
*/
public int getEditingRow() {
return editingColumn == -1 || isTreeColumn(editingColumn) ? -1 : editingRow;
}
/**
* Overridden to pass the new rowHeight to the tree.
*/
public void setRowHeight(int rowHeight) {
super.setRowHeight(rowHeight);
if (myTree != null && myTree.getRowHeight() < rowHeight) {
myTree.setRowHeight(getRowHeight());
}
}
/**
* @return the tree that is being shared between the model.
*/
public TreeTableTree getTree() {
return myTree;
}
protected void processKeyEvent(KeyEvent e){
int keyCode = e.getKeyCode();
final int selColumn = columnModel.getSelectionModel().getAnchorSelectionIndex();
boolean treeHasFocus = selColumn >= 0 && isTreeColumn(selColumn);
boolean oneRowSelected = getSelectedRowCount() == 1;
if(treeHasFocus && oneRowSelected && ((keyCode == KeyEvent.VK_LEFT) || (keyCode == KeyEvent.VK_RIGHT))){
TreePath selectionPath = myTree.getSelectionPath();
myTree._processKeyEvent(e);
myTree.setSelectionPath(selectionPath);
}
else{
super.processKeyEvent(e);
}
}
/**
* ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
* to listen for changes in the ListSelectionModel it maintains. Once
* a change in the ListSelectionModel happens, the paths are updated
* in the DefaultTreeSelectionModel.
*/
private class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
/** Set to true when we are updating the ListSelectionModel. */
protected boolean updatingListSelectionModel;
public ListToTreeSelectionModelWrapper() {
super();
getListSelectionModel().addListSelectionListener(createListSelectionListener());
}
/**
* @return the list selection model. ListToTreeSelectionModelWrapper
* listens for changes to this model and updates the selected paths
* accordingly.
*/
ListSelectionModel getListSelectionModel() {
return listSelectionModel;
}
/**
* This is overriden to set <code>updatingListSelectionModel</code>
* and message super. This is the only place DefaultTreeSelectionModel
* alters the ListSelectionModel.
*/
public void resetRowSelection() {
if (!updatingListSelectionModel) {
updatingListSelectionModel = true;
try {
Set<Integer> selectedRows = new HashSet<Integer>();
int min = listSelectionModel.getMinSelectionIndex();
int max = listSelectionModel.getMaxSelectionIndex();
if (min != -1 && max != -1) {
for (int counter = min; counter <= max; counter++) {
if (listSelectionModel.isSelectedIndex(counter)) {
selectedRows.add(new Integer(counter));
}
}
}
super.resetRowSelection();
listSelectionModel.clearSelection();
for (final Object selectedRow : selectedRows) {
Integer row = (Integer)selectedRow;
listSelectionModel.addSelectionInterval(row.intValue(), row.intValue());
}
}
finally {
updatingListSelectionModel = false;
}
}
// Notice how we don't message super if
// updatingListSelectionModel is true. If
// updatingListSelectionModel is true, it implies the
// ListSelectionModel has already been updated and the
// paths are the only thing that needs to be updated.
}
/**
* @return a newly created instance of ListSelectionHandler.
*/
protected ListSelectionListener createListSelectionListener() {
return new ListSelectionHandler();
}
/**
* If <code>updatingListSelectionModel</code> is false, this will
* reset the selected paths from the selected rows in the list
* selection model.
*/
protected void updateSelectedPathsFromSelectedRows() {
if (!updatingListSelectionModel) {
updatingListSelectionModel = true;
try {
// This is way expensive, ListSelectionModel needs an
// enumerator for iterating.
int min = listSelectionModel.getMinSelectionIndex();
int max = listSelectionModel.getMaxSelectionIndex();
clearSelection();
if (min != -1 && max != -1) {
for (int counter = min; counter <= max; counter++) {
if (listSelectionModel.isSelectedIndex(counter)) {
TreePath selPath = myTree.getPathForRow(counter);
if (selPath != null) {
addSelectionPath(selPath);
}
}
}
}
}
finally {
updatingListSelectionModel = false;
}
}
}
/**
* Class responsible for calling updateSelectedPathsFromSelectedRows
* when the selection of the list changse.
*/
class ListSelectionHandler implements ListSelectionListener {
public void valueChanged(ListSelectionEvent e) {
updateSelectedPathsFromSelectedRows();
}
}
}
public boolean editCellAt(int row, int column, EventObject e) {
boolean editResult = super.editCellAt(row, column, e);
if (e instanceof MouseEvent && isTreeColumn(column)){
MouseEvent me = (MouseEvent)e;
int y = me.getY();
if (getRowHeight() != myTree.getRowHeight()) {
// fix y if row heights are not equal
// [todo]: review setRowHeight to synchronize heights correctly!
final Rectangle tableCellRect = getCellRect(row, column, true);
y = Math.min(y - tableCellRect.y, myTree.getRowHeight() - 1) + row * myTree.getRowHeight();
}
MouseEvent newEvent = new MouseEvent(myTree, me.getID(),
me.getWhen(), me.getModifiers(),
me.getX() - getCellRect(0, column, true).x,
y, me.getClickCount(),
me.isPopupTrigger()
);
myTree.dispatchEvent(newEvent);
// Some LAFs, for example, Aqua under MAC OS X
// expand tree node by MOUSE_RELEASED event. Unfortunately,
// it's not possible to find easy way to wedge in table's
// event sequense. Therefore we send "synthetic" release event.
if (newEvent.getID()==MouseEvent.MOUSE_PRESSED) {
MouseEvent newME2 = new MouseEvent(
myTree,
MouseEvent.MOUSE_RELEASED,
me.getWhen(), me.getModifiers(),
me.getX() - getCellRect(0, column, true).x,
y - getCellRect(0, column, true).y, me.getClickCount(),
me.isPopupTrigger()
);
myTree.dispatchEvent(newME2);
}
}
return editResult;
}
private boolean isTreeColumn(int column) {
return TreeTableModel.class.isAssignableFrom(getColumnClass(column));
}
public void addSelectedPath(TreePath path) {
int row = getTree().getRowForPath(path);
getTree().addSelectionPath(path);
getSelectionModel().addSelectionInterval(row, row);
}
public void removeSelectedPath(TreePath path) {
int row = getTree().getRowForPath(path);
getTree().removeSelectionPath(path);
getSelectionModel().removeSelectionInterval(row, row);
}
public TreeTableCellRenderer createTableRenderer(TreeTableModel treeTableModel) {
return new TreeTableCellRenderer(this, myTree);
}
public void setMinRowHeight(int i) {
setRowHeight(Math.max(getRowHeight(), i));
}
}