
org.hobsoft.symmetry.ui.Table Maven / Gradle / Ivy
Show all versions of symmetry-ui Show documentation
/*
* 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 org.hobsoft.symmetry.ui;
import java.beans.EventSetDescriptor;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hobsoft.symmetry.support.bean.EventSets;
import org.hobsoft.symmetry.ui.event.AbstractClosureEventListener;
import org.hobsoft.symmetry.ui.event.TableCellListener;
import org.hobsoft.symmetry.ui.event.TableEvent;
import org.hobsoft.symmetry.ui.event.TableHeaderListener;
import org.hobsoft.symmetry.ui.event.TableModelEvent;
import org.hobsoft.symmetry.ui.event.TableModelListener;
import org.hobsoft.symmetry.ui.functor.Closure;
import org.hobsoft.symmetry.ui.model.DefaultTableModel;
import org.hobsoft.symmetry.ui.model.TableModel;
import org.hobsoft.symmetry.ui.traversal.ComponentVisitor;
import org.hobsoft.symmetry.ui.traversal.HierarchicalComponentVisitor;
import org.hobsoft.symmetry.ui.traversal.HierarchicalComponentVisitor.EndVisit;
import org.hobsoft.symmetry.ui.view.LabelTableCellRenderer;
import org.hobsoft.symmetry.ui.view.LabelTableHeaderRenderer;
import org.hobsoft.symmetry.ui.view.TableCellRenderer;
import com.google.common.reflect.TypeToken;
import static org.hobsoft.symmetry.ui.internal.Preconditions.checkNonNegative;
import static org.hobsoft.symmetry.ui.traversal.ComponentVisitors.asTableVisitor;
import static org.hobsoft.symmetry.ui.traversal.ComponentVisitors.nullHierarchicalVisitor;
import static org.hobsoft.symmetry.ui.traversal.HierarchicalComponentVisitor.EndVisit.SKIP_SIBLINGS;
import static org.hobsoft.symmetry.ui.traversal.HierarchicalComponentVisitor.Visit.SKIP_CHILDREN;
import static org.hobsoft.symmetry.ui.traversal.Visits.nullEndVisit;
import static com.google.common.base.Preconditions.checkElementIndex;
import static com.google.common.base.Preconditions.checkNotNull;
/**
*
*
* @author Mark Hobson
*/
public class Table extends Component
{
// constants --------------------------------------------------------------
public static final String HEADER_EVENT = "header";
public static final String CELL_EVENT = "cell";
public static final String MODEL_PROPERTY = "model";
public static final String VISIBLE_ROW_COUNT_PROPERTY = "visibleRowCount";
public static final String FIRST_VISIBLE_ROW_INDEX_PROPERTY = "firstVisibleRowIndex";
public static final String HEADER_RENDERER_PROPERTY = "headerRenderer";
public static final String CELL_RENDERER_PROPERTY = "cellRenderer";
public static final String HEADER_LISTENER_PROPERTY = "headerListener";
public static final String CELL_LISTENER_PROPERTY = "cellListener";
private static final EventSetDescriptor CELL_EVENT_SET = EventSets.getDescriptor(Table.class, CELL_EVENT);
private static final int DEFAULT_VISIBLE_ROW_COUNT = 20;
// types ------------------------------------------------------------------
static class ClosureTableCellListener extends AbstractClosureEventListener
implements TableCellListener
{
// constructors -----------------------------------------------------------
public ClosureTableCellListener(Closure super TableEvent> closure)
{
super(closure);
}
// TableCellListener methods ----------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void tableCellSelected(TableEvent event)
{
getClosure().apply(event);
}
}
// fields -----------------------------------------------------------------
private final List columns;
private TableModel model;
private int visibleRows;
private int firstVisibleRow;
private TableCellRenderer headerRenderer;
private TableCellRenderer cellRenderer;
private Map, TableCellRenderer> cellRenderersByType;
// constructors -----------------------------------------------------------
public Table()
{
this(new DefaultTableModel());
}
public Table(TableModel model)
{
if (model == null)
{
model = new DefaultTableModel();
}
columns = new ArrayList();
this.model = model;
visibleRows = DEFAULT_VISIBLE_ROW_COUNT;
firstVisibleRow = 0;
headerRenderer = new LabelTableHeaderRenderer();
cellRenderer = new LabelTableCellRenderer();
}
// Component methods ------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public EndVisit accept(ComponentVisitor
visitor, P parameter) throws E
{
return acceptTable(visitor, Table.class, this, parameter);
}
// public methods ---------------------------------------------------------
public void addHeaderListener(TableHeaderListener listener)
{
addListener(TableHeaderListener.class, listener);
firePropertyChange(HEADER_LISTENER_PROPERTY, null, listener);
}
public void removeHeaderListener(TableHeaderListener listener)
{
removeListener(TableHeaderListener.class, listener);
firePropertyChange(HEADER_LISTENER_PROPERTY, listener, null);
}
public TableHeaderListener[] getHeaderListeners()
{
return getListeners(TableHeaderListener.class);
}
public int getHeaderListenerCount()
{
return getListenerCount(TableHeaderListener.class);
}
public void addCellListener(TableCellListener listener)
{
addListener(TableCellListener.class, listener);
firePropertyChange(CELL_LISTENER_PROPERTY, null, listener);
}
public void removeCellListener(TableCellListener listener)
{
removeListener(TableCellListener.class, listener);
firePropertyChange(CELL_LISTENER_PROPERTY, listener, null);
}
public TableCellListener[] getCellListeners()
{
return getListeners(TableCellListener.class);
}
public int getCellListenerCount()
{
return getListenerCount(TableCellListener.class);
}
/**
* Adds the specified closure to those that will be executed when this table is clicked.
*
* @param closure
* the closure that will be executed
* @return this table
*/
public Table onSelect(Closure super TableEvent> closure)
{
addCellListener(new ClosureTableCellListener(closure));
return this;
}
public void doSelect(int row, int column)
{
fireCellSelected(new TableEvent(this, row, column));
}
public Table onChange(final Closure super TableModelEvent> closure)
{
getModel().addTableModelListener(new TableModelListener()
{
@Override
public void tableChanged(TableModelEvent event)
{
closure.apply(event);
}
});
return this;
}
public TableModel getModel()
{
return model;
}
public void setModel(TableModel model)
{
if (model == null)
{
model = new DefaultTableModel();
}
TableModel oldModel = this.model;
this.model = model;
firePropertyChange(MODEL_PROPERTY, oldModel, model);
}
public int getFirstVisibleRowIndex()
{
return firstVisibleRow;
}
public void setFirstVisibleRowIndex(int firstVisibleRow)
{
// TODO: can we achieve this without hitting the model?
// checkElementIndex(firstVisibleRow, getModel().getRowCount(), "firstVisibleRow");
checkNonNegative(firstVisibleRow, "firstVisibleRow");
int oldFirstVisibleRow = this.firstVisibleRow;
this.firstVisibleRow = firstVisibleRow;
firePropertyChange(FIRST_VISIBLE_ROW_INDEX_PROPERTY, oldFirstVisibleRow, firstVisibleRow);
}
public int getVisibleRowCount()
{
return visibleRows;
}
public void setVisibleRowCount(int visibleRows)
{
checkNonNegative(visibleRows, "visibleRows");
int oldVisibleRows = this.visibleRows;
this.visibleRows = visibleRows;
firePropertyChange(VISIBLE_ROW_COUNT_PROPERTY, oldVisibleRows, visibleRows);
}
public TableCellRenderer getHeaderRenderer()
{
return headerRenderer;
}
public void setHeaderRenderer(TableCellRenderer headerRenderer)
{
checkNotNull(headerRenderer, "headerRenderer cannot be null");
TableCellRenderer oldHeaderRenderer = this.headerRenderer;
this.headerRenderer = headerRenderer;
firePropertyChange(HEADER_RENDERER_PROPERTY, oldHeaderRenderer, headerRenderer);
}
public TableCellRenderer getCellRenderer(int row, int column)
{
// try column first
TableCellRenderer renderer = getCellRendererForColumn(column);
// failing that try column type
if (renderer == null)
{
Class> type = model.getColumnClass(column);
renderer = getCellRendererForType(type);
}
// failing that use default
if (renderer == null)
{
renderer = cellRenderer;
}
return renderer;
}
public void setCellRenderer(TableCellRenderer cellRenderer)
{
TableCellRenderer oldCellRenderer = this.cellRenderer;
this.cellRenderer = cellRenderer;
firePropertyChange(CELL_RENDERER_PROPERTY, oldCellRenderer, cellRenderer);
}
public void setCellRenderer(Class> klass, TableCellRenderer cellRenderer)
{
if (cellRenderersByType == null)
{
cellRenderersByType = new HashMap, TableCellRenderer>();
}
TableCellRenderer oldCellRenderer = cellRenderersByType.put(klass, cellRenderer);
// TODO: add class to event somehow?
firePropertyChange(CELL_RENDERER_PROPERTY, oldCellRenderer, cellRenderer);
}
public List getColumns()
{
return new AbstractList()
{
@Override
public int size()
{
return getModel().getColumnCount();
}
@Override
public TableColumn get(int index)
{
return getColumn(index);
}
};
}
public TableColumn getColumn(int columnIndex)
{
checkColumnIndex(columnIndex);
// not threadsafe
TableColumn column = getColumnOrNull(columnIndex);
if (column == null)
{
ensureListSize(columns, columnIndex + 1);
column = new TableColumn(this, columnIndex);
columns.set(columnIndex, column);
}
return column;
}
// protected methods ------------------------------------------------------
protected static EndVisit acceptTable(ComponentVisitor visitor,
Class tableType, T table, P parameter) throws E
{
return acceptTable(visitor, TypeToken.of(tableType), table, parameter);
}
protected static EndVisit acceptTable(ComponentVisitor visitor,
TypeToken tableType, T table, P parameter) throws E
{
HierarchicalComponentVisitor subvisitor = visitor.visit(tableType, table, parameter);
if (subvisitor == null || subvisitor.visit(table, parameter) != SKIP_CHILDREN)
{
if (acceptHeader(visitor, subvisitor, table, parameter) != SKIP_SIBLINGS)
{
acceptBody(visitor, subvisitor, table, parameter);
}
}
return nullEndVisit(nullHierarchicalVisitor(subvisitor).endVisit(table, parameter));
}
protected final void fireCellSelected(TableEvent event)
{
fireEvent(CELL_EVENT_SET, event);
}
// private methods --------------------------------------------------------
private static EndVisit acceptHeader(ComponentVisitor visitor,
HierarchicalComponentVisitor subvisitor, T table, P parameter) throws E
{
if (asTableVisitor(subvisitor).visitHeader(table, parameter) != SKIP_CHILDREN)
{
int columns = table.getModel().getColumnCount();
for (int column = 0; column < columns; column++)
{
if (acceptHeaderCell(visitor, subvisitor, table, parameter, column) == SKIP_SIBLINGS)
{
break;
}
}
}
return nullEndVisit(asTableVisitor(subvisitor).endVisitHeader(table, parameter));
}
private static EndVisit acceptHeaderCell(ComponentVisitor visitor,
HierarchicalComponentVisitor subvisitor, T table, P parameter, int column) throws E
{
if (asTableVisitor(subvisitor).visitHeaderCell(table, parameter, column) != SKIP_CHILDREN)
{
TableCellRenderer renderer = getEffectiveHeaderRenderer(table, column);
Component headerComponent = renderer.getTableCellComponent(table, -1, column);
if (headerComponent != null)
{
// TODO: should we use the return value?
headerComponent.accept(visitor, parameter);
}
}
return nullEndVisit(asTableVisitor(subvisitor).endVisitHeaderCell(table, parameter, column));
}
private static EndVisit acceptBody(ComponentVisitor visitor,
HierarchicalComponentVisitor subvisitor, T table, P parameter) throws E
{
if (asTableVisitor(subvisitor).visitBody(table, parameter) != SKIP_CHILDREN)
{
int rows = table.getModel().getRowCount();
for (int row = 0; row < rows; row++)
{
if (acceptRow(visitor, subvisitor, table, parameter, row) == SKIP_SIBLINGS)
{
break;
}
}
}
return nullEndVisit(asTableVisitor(subvisitor).endVisitBody(table, parameter));
}
private static EndVisit acceptRow(ComponentVisitor visitor,
HierarchicalComponentVisitor subvisitor, T table, P parameter, int row) throws E
{
if (asTableVisitor(subvisitor).visitRow(table, parameter, row) != SKIP_CHILDREN)
{
int columns = table.getModel().getColumnCount();
for (int column = 0; column < columns; column++)
{
if (acceptCell(visitor, subvisitor, table, parameter, row, column) == SKIP_SIBLINGS)
{
break;
}
}
}
return nullEndVisit(asTableVisitor(subvisitor).endVisitRow(table, parameter, row));
}
private static EndVisit acceptCell(ComponentVisitor visitor,
HierarchicalComponentVisitor subvisitor, T table, P parameter, int row, int column) throws E
{
if (asTableVisitor(subvisitor).visitCell(table, parameter, row, column) != SKIP_CHILDREN)
{
TableCellRenderer renderer = table.getCellRenderer(row, column);
Component cellComponent = renderer.getTableCellComponent(table, row, column);
if (cellComponent != null)
{
// TODO: should we use the return value?
cellComponent.accept(visitor, parameter);
}
}
return nullEndVisit(asTableVisitor(subvisitor).endVisitCell(table, parameter, row, column));
}
private static TableCellRenderer getEffectiveHeaderRenderer(Table table, int columnIndex)
{
TableCellRenderer renderer = table.getColumn(columnIndex).getHeaderRenderer();
if (renderer == null)
{
renderer = table.getHeaderRenderer();
}
return renderer;
}
private TableCellRenderer getCellRendererForColumn(int columnIndex)
{
TableColumn column = getColumnOrNull(columnIndex);
return (column != null) ? column.getCellRenderer() : null;
}
private TableCellRenderer getCellRendererForType(Class> type)
{
if (cellRenderersByType == null)
{
return null;
}
// try superclasses first
for (Class> supertype = type; supertype != null; supertype = supertype.getSuperclass())
{
TableCellRenderer renderer = cellRenderersByType.get(supertype);
if (renderer != null)
{
return renderer;
}
}
// then try interfaces
for (Class> supertype = type; supertype != null; supertype = supertype.getSuperclass())
{
for (Class> iface : supertype.getInterfaces())
{
TableCellRenderer renderer = cellRenderersByType.get(iface);
if (renderer != null)
{
return renderer;
}
}
}
return null;
}
private TableColumn getColumnOrNull(int columnIndex)
{
TableColumn column;
if (columnIndex < columns.size())
{
column = columns.get(columnIndex);
}
else
{
column = null;
}
return column;
}
// TODO: use ui.model.Utilities.setSize
private static void ensureListSize(List list, int size)
{
ensureListSize(list, size, null);
}
private static void ensureListSize(List list, int size, T object)
{
int delta = size - list.size();
if (delta > 0)
{
list.addAll(Collections.nCopies(delta, object));
}
}
private void checkColumnIndex(int columnIndex)
{
checkElementIndex(columnIndex, getModel().getColumnCount(), "columnIndex");
}
}