/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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.apache.pivot.wtk;
import java.io.IOException;
import java.net.URL;
import java.util.Comparator;
import java.util.Iterator;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.ListListener;
import org.apache.pivot.collections.Map;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.serialization.JSONSerializer;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.util.Filter;
import org.apache.pivot.util.ImmutableIterator;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.util.Vote;
import org.apache.pivot.wtk.content.TableViewCellRenderer;
/**
* Component that displays a sequence of items partitioned into columns,
* optionally allowing a user to select one or more rows.
*/
public class TableView extends Component {
/**
* Contains information about a table column.
*/
public static final class Column {
private TableView tableView = null;
private String name = null;
private Object headerData = null;
private int width = 0;
private int minimumWidth = 0;
private int maximumWidth = Integer.MAX_VALUE;
private boolean relative = false;
private Object filter = null;
private CellRenderer cellRenderer = DEFAULT_CELL_RENDERER;
private static final CellRenderer DEFAULT_CELL_RENDERER = new TableViewCellRenderer();
/**
* Default column width.
*/
public static final int DEFAULT_WIDTH = 100;
/**
* Creates an empty column.
*/
public Column() {
this(null, null, DEFAULT_WIDTH, false);
}
/**
* Creates a new column with no header data and a fixed default width.
*
* @param name
* The column name.
*/
public Column(String name) {
this(name, null, DEFAULT_WIDTH, false);
}
/**
* Creates a new column with a fixed default width.
*
* @param name
* The column name.
*
* @param headerData
* The column header data.
*/
public Column(String name, Object headerData) {
this(name, headerData, DEFAULT_WIDTH, false);
}
/**
* Creates a new column with a fixed width.
*
* @param name
* The column name.
*
* @param headerData
* The column header data.
*
* @param width
* The width of the column.
*/
public Column(String name, Object headerData, int width) {
this(name, headerData, width, false);
}
/**
* Creates a new column.
*
* @param name
* The column name.
*
* @param headerData
* The column header data.
*
* @param width
* The width of the column.
*
* @param relative
* If <tt>true</tt>, specifies a relative column width; otherwise,
* specifies a fixed column width.
*/
public Column(String name, Object headerData, int width, boolean relative) {
setName(name);
setHeaderData(headerData);
setWidth(width, relative);
}
/**
* Returns the table view with which this column is associated.
*
* @return
* The column's table view, or <tt>null</tt> if the column does not
* currently belong to a table.
*/
public TableView getTableView() {
return tableView;
}
/**
* Sets the table view with which this column is associated.
*
* @param tableView
* The column's table view, or <tt>null</tt> if the column does not
* currently belong to a table.
*/
private void setTableView(TableView tableView) {
this.tableView = tableView;
}
/**
* Returns the column name.
*
* @return
* The column name.
*/
public String getName() {
return name;
}
/**
* Sets the column name.
*
* @param name
* The column name.
*/
public void setName(String name) {
String previousName = this.name;
if (previousName != name) {
this.name = name;
if (tableView != null) {
tableView.tableViewColumnListeners.columnNameChanged(this,
previousName);
}
}
}
/**
* Returns the column header data.
*
* @return
* The column header data, or <tt>null</tt> if the column has no
* header data.
*/
public Object getHeaderData() {
return headerData;
}
/**
* Sets the column header data.
*
* @param headerData
* The column header data, or <tt>null</tt> for no header data.
*/
public void setHeaderData(Object headerData) {
Object previousHeaderData = this.headerData;
if (previousHeaderData != headerData) {
this.headerData = headerData;
if (tableView != null) {
tableView.tableViewColumnListeners.columnHeaderDataChanged(this,
previousHeaderData);
}
}
}
/**
* Returns the column width.
*
* @return
* The width of the column.
*/
public int getWidth() {
return width;
}
/**
* Returns the relative flag.
*
* @return
* <tt>true</tt> if the column width is relative, <tt>false</tt> if it
* is fixed.
*/
public boolean isRelative() {
return relative;
}
/**
* Set the column width.
*
* @param width
* The absolute width of the column.
*/
public void setWidth(int width) {
setWidth(width, false);
}
/**
* Set the column width.
*
* @param width
* The encoded width of the row. If the string ends with the '*'
* character, it is treated as a relative value. Otherwise, it is
* considered an absolute value.
*/
public void setWidth(String width) {
boolean relative = false;
if (width.endsWith("*")) {
relative = true;
width = width.substring(0, width.length() - 1);
}
setWidth(Integer.parseInt(width), relative);
}
/**
* Sets the column width.
*
* @param width
* The width of the column.
*
* @param relative
* <tt>true</tt> if the column width is relative, <tt>false</tt> if it
* is fixed.
*/
public void setWidth(int width, boolean relative) {
if (width < (relative ? 0 : -1)) {
throw new IllegalArgumentException();
}
int previousWidth = this.width;
boolean previousRelative = this.relative;
if (previousWidth != width
|| previousRelative != relative) {
this.width = width;
this.relative = relative;
if (tableView != null) {
tableView.tableViewColumnListeners.columnWidthChanged(this,
previousWidth, previousRelative);
}
}
}
/**
* Gets the minimum and maximum widths to which the column can size.
*/
public Limits getWidthLimits() {
return new Limits(minimumWidth, maximumWidth);
}
/**
* Sets the minimum and maximum widths the column can size to.
*
* @param minimumWidth
* Column width cannot be smaller than this size.
*
* @param maximumWidth
* Column width cannot be greater than this size.
*/
public void setWidthLimits(int minimumWidth, int maximumWidth) {
if (minimumWidth < 0) {
throw new IllegalArgumentException("Minimum width is negative.");
}
if (maximumWidth < minimumWidth) {
throw new IllegalArgumentException("Maximum width is smaller than minimum width.");
}
int previousMinimumWidth = this.minimumWidth;
int previousMaximumWidth = this.maximumWidth;
if (minimumWidth != previousMinimumWidth
|| maximumWidth != previousMaximumWidth) {
this.minimumWidth = minimumWidth;
this.maximumWidth = maximumWidth;
if (tableView != null) {
tableView.tableViewColumnListeners.columnWidthLimitsChanged(this,
previousMinimumWidth, previousMaximumWidth);
}
}
}
/**
* Sets the minimum and maximum widths to which the column can size.
*
* @param widthLimits
* The new width limits.
*/
public void setWidthLimits(Limits widthLimits) {
setWidthLimits(widthLimits.min, widthLimits.max);
}
/**
* Gets the minimum width the column is allowed to be.
*
* @return
* Minimum column width.
*/
public int getMinimumWidth() {
return minimumWidth;
}
/**
* Sets the minimum width the column is allowed to be.
*
* @param minimumWidth
* Minimum column width.
*/
public void setMinimumWidth(int minimumWidth) {
setWidthLimits(minimumWidth, maximumWidth);
}
/**
* Get the maximum width the column is allowed to be.
*
* @return
* Maximum column width.
*/
public int getMaximumWidth() {
return maximumWidth;
}
/**
* Set the maximum width the column is allowed to be.
*
* @param maximumWidth
* Maximum column width.
*/
public void setMaximumWidth(int maximumWidth) {
setWidthLimits(minimumWidth, maximumWidth);
}
/**
* Returns the column's filter.
*
* @return
* The column's filter, or <tt>null</tt> if the column does not have
* a filter.
*/
public Object getFilter() {
return filter;
}
/**
* Sets the column's filter.
*
* @param filter
* The column's filter, or <tt>null</tt> for no filter.
*/
public void setFilter(Object filter) {
Object previousFilter = this.filter;
if (previousFilter != filter) {
this.filter = filter;
if (tableView != null) {
tableView.tableViewColumnListeners.columnFilterChanged(this,
previousFilter);
}
}
}
/**
* Returns the column's cell renderer.
*
* @return
* The cell renderer that is used to draw the contents of this column.
*/
public CellRenderer getCellRenderer() {
return cellRenderer;
}
/**
* Sets the column's cell renderer.
*
* @param cellRenderer
* The cell renderer that is used to draw the contents of this column.
*/
public void setCellRenderer(CellRenderer cellRenderer) {
if (cellRenderer == null) {
throw new IllegalArgumentException("cellRenderer is null.");
}
CellRenderer previousCellRenderer = this.cellRenderer;
if (previousCellRenderer != cellRenderer) {
this.cellRenderer = cellRenderer;
if (tableView != null) {
tableView.tableViewColumnListeners.columnCellRendererChanged(this,
previousCellRenderer);
}
}
}
}
/**
* Enumeration defining supported selection modes.
*/
public enum SelectMode {
/**
* Selection is disabled.
*/
NONE,
/**
* A single index may be selected at a time.
*/
SINGLE,
/**
* Multiple indexes may be concurrently selected.
*/
MULTI
}
/**
* Table cell renderer interface.
*/
public interface CellRenderer extends Renderer {
/**
* Prepares the renderer for layout or paint.
*
* @param value
* The cell value to render, or <tt>null</tt> if called to calculate
* preferred height for skins that assume a fixed renderer height.
*
* @param rowIndex
* The index of the row being rendered, or <tt>-1</tt> if <tt>value</tt>
* is <tt>null</tt>.
*
* @param columnIndex
* The index of the column being rendered.
*
* @param tableView
* The table view that contains the cell.
*
* @param columnName
* The name of the column being rendered.
*
* @param rowSelected
* If <tt>true</tt>, the row is selected.
*
* @param rowHighlighted
* If <tt>true</tt>, the row is highlighted.
*
* @param rowDisabled
* If <tt>true</tt>, the row is disabled.
*/
public void render(Object value, int rowIndex, int columnIndex,
TableView tableView, String columnName,
boolean rowSelected, boolean rowHighlighted, boolean rowDisabled);
}
/**
* Table row editor interface.
*/
public interface RowEditor extends Editor {
/**
* Row editor listener list.
*/
public static class RowEditorListenerList
extends ListenerList<RowEditorListener>
implements RowEditorListener {
@Override
public Vote previewEditRow(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
Vote vote = Vote.APPROVE;
for (RowEditorListener listener : this) {
vote = vote.tally(listener.previewEditRow(rowEditor,
tableView, rowIndex, columnIndex));
}
return vote;
}
@Override
public void editRowVetoed(RowEditor rowEditor, Vote reason) {
for (RowEditorListener listener : this) {
listener.editRowVetoed(rowEditor, reason);
}
}
@Override
public void rowEditing(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
for (RowEditorListener listener : this) {
listener.rowEditing(rowEditor, tableView, rowIndex,
columnIndex);
}
}
@Override
public Vote previewSaveChanges(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex, Dictionary<String, Object> changes) {
Vote vote = Vote.APPROVE;
for (RowEditorListener listener : this) {
vote = vote.tally(listener.previewSaveChanges(rowEditor,
tableView, rowIndex, columnIndex, changes));
}
return vote;
}
@Override
public void saveChangesVetoed(RowEditor rowEditor, Vote reason) {
for (RowEditorListener listener : this) {
listener.saveChangesVetoed(rowEditor, reason);
}
}
@Override
public void changesSaved(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
for (RowEditorListener listener : this) {
listener.changesSaved(rowEditor, tableView, rowIndex,
columnIndex);
}
}
@Override
public void editCancelled(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
for (RowEditorListener listener : this) {
listener.editCancelled(rowEditor, tableView, rowIndex,
columnIndex);
}
}
}
/**
* Notifies the editor that editing should begin. If the editor is
* currently installed on the table view, the skin may call this method
* when the user executes the appropriate gesture (as defined by the
* skin).
*
* @param tableView
* The table view
*
* @param rowIndex
* The row index of the cell to edit
*
* @param columnIndex
* The column index of the cell to edit
*
* @see
* #setRowEditor(RowEditor)
*/
public void editRow(TableView tableView, int rowIndex, int columnIndex);
/**
* Gets the row editor listener list.
*/
public ListenerList<RowEditorListener> getRowEditorListeners();
}
/**
* The row editor listener interface. This provides callers with
* notifications about a row editor's activity.
*/
public interface RowEditorListener {
/**
* Row editor listener adapter.
*/
public static class Adapter implements RowEditorListener {
@Override
public Vote previewEditRow(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
return Vote.APPROVE;
}
@Override
public void editRowVetoed(RowEditor rowEditor, Vote reason) {
}
@Override
public void rowEditing(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
}
@Override
public Vote previewSaveChanges(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex, Dictionary<String, Object> changes) {
return Vote.APPROVE;
}
@Override
public void saveChangesVetoed(RowEditor rowEditor, Vote reason) {
}
@Override
public void changesSaved(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
}
@Override
public void editCancelled(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex) {
}
}
/**
* Called to preview a row edit.
*
* @param rowEditor
* The row editor
*
* @param tableView
* The table view containing the row to be edited.
*
* @param rowIndex
* The index of the row to edit.
*
* @param columnIndex
* The index of the column to edit.
*
* @return
* A vote on whether editing should be allowed to begin.
*/
public Vote previewEditRow(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex);
/**
* Called when a row edit was vetoed by a listener in the preview
* event.
*
* @param rowEditor
* The row editor
*
* @param reason
* The reason for the veto
*/
public void editRowVetoed(RowEditor rowEditor, Vote reason);
/**
* Called when editing has begun.
*
* @param rowEditor
* The row editor
*
* @param tableView
* The table view containing the row being edited.
*
* @param rowIndex
* The index of the row being edited.
*
* @param columnIndex
* The index of the column being edited.
*/
public void rowEditing(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex);
/**
* Called to preview a save.
*
* @param rowEditor
* The row editor
*
* @param tableView
* The table view containing the row being edited.
*
* @param rowIndex
* The index of the row being edited.
*
* @param columnIndex
* The index of the column being edited.
*
* @param changes
* The proposed changes, indexed by table view column name. The type of
* each entry in this dictionary will depend on the editor
* implementation.
*
* @return
* A vote on whether the changes should be allowed to be saved.
*/
public Vote previewSaveChanges(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex, Dictionary<String, Object> changes);
/**
* Called when a save was vetoed by a listener in the preview event.
*
* @param rowEditor
* The row editor
*
* @param reason
* The reason for the veto
*/
public void saveChangesVetoed(RowEditor rowEditor, Vote reason);
/**
* Called when changes have been saved.
*
* @param rowEditor
* The row editor
*
* @param tableView
* The table view containing the row that was edited.
*
* @param rowIndex
* The index of the row that was edited.
*
* @param columnIndex
* The index of the column that was edited.
*/
public void changesSaved(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex);
/**
* Called when an edit has been cancelled.
*
* @param rowEditor
* The row editor
*
* @param tableView
* The table view containing the row that was being edited.
*
* @param rowIndex
* The index of the row that was being edited.
*
* @param columnIndex
* The index of the column that was being edited.
*/
public void editCancelled(RowEditor rowEditor, TableView tableView,
int rowIndex, int columnIndex);
}
/**
* Table view skin interface. Table view skins must implement this.
*/
public interface Skin {
public int getRowAt(int y);
public int getColumnAt(int x);
public Bounds getRowBounds(int rowIndex);
public Bounds getColumnBounds(int columnIndex);
public Bounds getCellBounds(int rowIndex, int columnIndex);
}
/**
* Column sequence implementation.
*/
public final class ColumnSequence implements Sequence<Column>, Iterable<Column> {
@Override
public int add(Column column) {
int i = getLength();
insert(column, i);
return i;
}
@Override
public void insert(Column column, int index) {
if (column == null) {
throw new IllegalArgumentException("column is null.");
}
if (column.getTableView() != null) {
throw new IllegalArgumentException("column is already in use by another table view.");
}
columns.insert(column, index);
column.setTableView(TableView.this);
tableViewColumnListeners.columnInserted(TableView.this, index);
}
@Override
public Column update(int index, Column column) {
throw new UnsupportedOperationException();
}
@Override
public int remove(Column column) {
int index = indexOf(column);
if (index != -1) {
remove(index, 1);
}
return index;
}
@Override
public Sequence<Column> remove(int index, int count) {
Sequence<Column> removed = columns.remove(index, count);
if (count > 0) {
for (int i = 0, n = removed.getLength(); i < n; i++) {
removed.get(i).setTableView(null);
}
tableViewColumnListeners.columnsRemoved(TableView.this, index, removed);
}
return removed;
}
@Override
public Column get(int index) {
return columns.get(index);
}
@Override
public int indexOf(Column column) {
return columns.indexOf(column);
}
@Override
public int getLength() {
return columns.getLength();
}
@Override
public Iterator<Column> iterator() {
return new ImmutableIterator<Column>(columns.iterator());
}
}
/**
* Table view listener list.
*/
private static class TableViewListenerList extends ListenerList<TableViewListener>
implements TableViewListener {
@Override
public void tableDataChanged(TableView tableView, List<?> previousTableData) {
for (TableViewListener listener : this) {
listener.tableDataChanged(tableView, previousTableData);
}
}
@Override
public void columnSourceChanged(TableView tableView, TableView previousColumnSource) {
for (TableViewListener listener : this) {
listener.columnSourceChanged(tableView, previousColumnSource);
}
}
@Override
public void rowEditorChanged(TableView tableView,
TableView.RowEditor previousRowEditor) {
for (TableViewListener listener : this) {
listener.rowEditorChanged(tableView, previousRowEditor);
}
}
@Override
public void selectModeChanged(TableView tableView, SelectMode previousSelectMode) {
for (TableViewListener listener : this) {
listener.selectModeChanged(tableView, previousSelectMode);
}
}
@Override
public void disabledRowFilterChanged(TableView tableView, Filter<?> previousDisabledRowFilter) {
for (TableViewListener listener : this) {
listener.disabledRowFilterChanged(tableView, previousDisabledRowFilter);
}
}
}
/**
* Sort dictionary implementation.
*/
public final class SortDictionary implements Dictionary<String, SortDirection>, Iterable<String> {
public SortDirection get(String columnName) {
return sortMap.get(columnName);
}
public SortDirection put(String columnName, SortDirection sortDirection) {
SortDirection previousSortDirection;
if (sortDirection == null) {
previousSortDirection = remove(columnName);
} else {
boolean update = containsKey(columnName);
previousSortDirection = sortMap.put(columnName, sortDirection);
if (update) {
tableViewSortListeners.sortUpdated(TableView.this, columnName, previousSortDirection);
} else {
sortList.add(columnName);
tableViewSortListeners.sortAdded(TableView.this, columnName);
}
}
return previousSortDirection;
}
public SortDirection remove(String columnName) {
SortDirection sortDirection = null;
if (containsKey(columnName)) {
sortDirection = sortMap.remove(columnName);
sortList.remove(columnName);
tableViewSortListeners.sortRemoved(TableView.this, columnName, sortDirection);
}
return sortDirection;
}
public boolean containsKey(String columnName) {
return sortMap.containsKey(columnName);
}
public boolean isEmpty() {
return sortMap.isEmpty();
}
public Dictionary.Pair<String, SortDirection> get(int index) {
String columnName = sortList.get(index);
SortDirection sortDirection = sortMap.get(columnName);
return new Dictionary.Pair<String, SortDirection>(columnName, sortDirection);
}
public int getLength() {
return sortList.getLength();
}
public Iterator<String> iterator() {
return sortList.iterator();
}
}
/**
* Table view column listener list.
*/
private static class TableViewColumnListenerList extends ListenerList<TableViewColumnListener>
implements TableViewColumnListener {
@Override
public void columnInserted(TableView tableView, int index) {
for (TableViewColumnListener listener : this) {
listener.columnInserted(tableView, index);
}
}
@Override
public void columnsRemoved(TableView tableView, int index, Sequence<TableView.Column> columns) {
for (TableViewColumnListener listener : this) {
listener.columnsRemoved(tableView, index, columns);
}
}
@Override
public void columnNameChanged(Column column, String previousName) {
for (TableViewColumnListener listener : this) {
listener.columnNameChanged(column, previousName);
}
}
@Override
public void columnHeaderDataChanged(Column column, Object previousHeaderData) {
for (TableViewColumnListener listener : this) {
listener.columnHeaderDataChanged(column, previousHeaderData);
}
}
@Override
public void columnWidthChanged(Column column, int previousWidth, boolean previousRelative) {
for (TableViewColumnListener listener : this) {
listener.columnWidthChanged(column, previousWidth, previousRelative);
}
}
@Override
public void columnWidthLimitsChanged(Column column, int previousMinimumWidth, int previousMaximumWidth) {
for (TableViewColumnListener listener : this) {
listener.columnWidthLimitsChanged(column, previousMinimumWidth, previousMaximumWidth);
}
}
@Override
public void columnFilterChanged(Column column, Object previousFilter) {
for (TableViewColumnListener listener : this) {
listener.columnFilterChanged(column, previousFilter);
}
}
@Override
public void columnCellRendererChanged(Column column, TableView.CellRenderer previousCellRenderer) {
for (TableViewColumnListener listener : this) {
listener.columnCellRendererChanged(column, previousCellRenderer);
}
}
}
/**
* Table view item listener list.
*/
private static class TableViewRowListenerList extends ListenerList<TableViewRowListener>
implements TableViewRowListener {
@Override
public void rowInserted(TableView tableView, int index) {
for (TableViewRowListener listener : this) {
listener.rowInserted(tableView, index);
}
}
@Override
public void rowsRemoved(TableView tableView, int index, int count) {
for (TableViewRowListener listener : this) {
listener.rowsRemoved(tableView, index, count);
}
}
@Override
public void rowUpdated(TableView tableView, int index) {
for (TableViewRowListener listener : this) {
listener.rowUpdated(tableView, index);
}
}
@Override
public void rowsCleared(TableView tableView) {
for (TableViewRowListener listener : this) {
listener.rowsCleared(tableView);
}
}
@Override
public void rowsSorted(TableView tableView) {
for (TableViewRowListener listener : this) {
listener.rowsSorted(tableView);
}
}
}
/**
* Table view selection detail listener list.
*/
private static class TableViewSelectionListenerList extends ListenerList<TableViewSelectionListener>
implements TableViewSelectionListener {
@Override
public void selectedRangeAdded(TableView tableView, int rangeStart, int rangeEnd) {
for (TableViewSelectionListener listener : this) {
listener.selectedRangeAdded(tableView, rangeStart, rangeEnd);
}
}
@Override
public void selectedRangeRemoved(TableView tableView, int rangeStart, int rangeEnd) {
for (TableViewSelectionListener listener : this) {
listener.selectedRangeRemoved(tableView, rangeStart, rangeEnd);
}
}
@Override
public void selectedRangesChanged(TableView tableView, Sequence<Span> previousSelection) {
for (TableViewSelectionListener listener : this) {
listener.selectedRangesChanged(tableView, previousSelection);
}
}
}
private static class TableViewSortListenerList extends ListenerList<TableViewSortListener>
implements TableViewSortListener {
public void sortAdded(TableView tableView, String columnName) {
for (TableViewSortListener listener : this) {
listener.sortAdded(tableView, columnName);
}
}
public void sortUpdated(TableView tableView, String columnName,
SortDirection previousSortDirection) {
for (TableViewSortListener listener : this) {
listener.sortUpdated(tableView, columnName, previousSortDirection);
}
}
public void sortRemoved(TableView tableView, String columnName,
SortDirection sortDirection) {
for (TableViewSortListener listener : this) {
listener.sortRemoved(tableView, columnName, sortDirection);
}
}
public void sortChanged(TableView tableView) {
for (TableViewSortListener listener : this) {
listener.sortChanged(tableView);
}
}
}
private ArrayList<Column> columns = new ArrayList<Column>();
private ColumnSequence columnSequence = new ColumnSequence();
private List<?> tableData = null;
private ListListener<Object> tableDataListener = new ListListener<Object>() {
@Override
public void itemInserted(List<Object> list, int index) {
// Increment selected ranges
selectedRanges.insertIndex(index);
// Notify listeners that items were inserted
tableViewRowListeners.rowInserted(TableView.this, index);
}
@Override
public void itemsRemoved(List<Object> list, int index, Sequence<Object> items) {
int count = items.getLength();
// Decrement selected ranges
selectedRanges.removeIndexes(index, count);
// Notify listeners that items were removed
tableViewRowListeners.rowsRemoved(TableView.this, index, count);
}
@Override
public void itemUpdated(List<Object> list, int index, Object previousItem) {
tableViewRowListeners.rowUpdated(TableView.this, index);
}
@Override
public void listCleared(List<Object> list) {
// All items were removed; clear the selection and notify
// listeners
selectedRanges.clear();
tableViewRowListeners.rowsCleared(TableView.this);
}
@Override
public void comparatorChanged(List<Object> list,
Comparator<Object> previousComparator) {
if (list.getComparator() != null) {
selectedRanges.clear();
tableViewRowListeners.rowsSorted(TableView.this);
}
}
};
private TableView columnSource = null;
private ListSelection selectedRanges = new ListSelection();
private SelectMode selectMode = SelectMode.SINGLE;
private HashMap<String, SortDirection> sortMap = new HashMap<String, SortDirection>();
private ArrayList<String> sortList = new ArrayList<String>();
private SortDictionary sortDictionary = new SortDictionary();
private Filter<?> disabledRowFilter = null;
private RowEditor rowEditor = null;
private TableViewListenerList tableViewListeners = new TableViewListenerList();
private TableViewColumnListenerList tableViewColumnListeners = new TableViewColumnListenerList();
private TableViewRowListenerList tableViewRowListeners = new TableViewRowListenerList();
private TableViewSelectionListenerList tableViewSelectionListeners = new TableViewSelectionListenerList();
private TableViewSortListenerList tableViewSortListeners = new TableViewSortListenerList();
public static final String COLUMN_NAME_KEY = "columnName";
public static final String SORT_DIRECTION_KEY = "sortDirection";
/**
* Creates a new table view populated with an empty array list.
*/
public TableView() {
this(new ArrayList<Object>());
}
/**
* Creates a new table view populated with the given table data.
*
* @param tableData
*/
public TableView(List<?> tableData) {
setTableData(tableData);
installThemeSkin(TableView.class);
}
@Override
protected void setSkin(org.apache.pivot.wtk.Skin skin) {
if (!(skin instanceof TableView.Skin)) {
throw new IllegalArgumentException("Skin class must implement "
+ TableView.Skin.class.getName());
}
super.setSkin(skin);
}
/**
* Returns the table column sequence.
*
* @return
* The table column sequence.
*/
public ColumnSequence getColumns() {
ColumnSequence columnSequence = this.columnSequence;
if (columnSource != null) {
columnSequence = columnSource.getColumns();
}
return columnSequence;
}
/**
* Returns the table data.
*
* @return
* The data currently presented by the table view.
*/
public List<?> getTableData() {
return this.tableData;
}
/**
* Sets the table data.
*
* @param tableData
* The data to be presented by the table view.
*/
@SuppressWarnings("unchecked")
public void setTableData(List<?> tableData) {
if (tableData == null) {
throw new IllegalArgumentException("tableData is null.");
}
List<?> previousTableData = this.tableData;
if (previousTableData != tableData) {
if (previousTableData != null) {
// Clear any existing selection
selectedRanges.clear();
((List<Object>)previousTableData).getListListeners().remove(tableDataListener);
}
((List<Object>)tableData).getListListeners().add(tableDataListener);
// Update the list data and fire change event
this.tableData = tableData;
tableViewListeners.tableDataChanged(this, previousTableData);
}
}
/**
* Sets the table data.
*
* @param tableData
* A JSON string (must begin with <tt>[</tt> and end with <tt>]</tt>)
* denoting the data to be presented by the table view.
*/
public final void setTableData(String tableData) {
if (tableData == null) {
throw new IllegalArgumentException("tableData is null.");
}
try {
setTableData(JSONSerializer.parseList(tableData));
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
}
}
/**
* Sets the table data.
*
* @param tableData
* A URL referring to a JSON file containing the data to be presented by
* the table view.
*/
public void setTableData(URL tableData) {
if (tableData == null) {
throw new IllegalArgumentException("tableData is null.");
}
JSONSerializer jsonSerializer = new JSONSerializer();
try {
setTableData((List<?>)jsonSerializer.readObject(tableData.openStream()));
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
} catch (IOException exception) {
throw new IllegalArgumentException(exception);
}
}
public TableView getColumnSource() {
return columnSource;
}
public void setColumnSource(TableView columnSource) {
TableView previousColumnSource = this.columnSource;
if (previousColumnSource != columnSource) {
this.columnSource = columnSource;
tableViewListeners.columnSourceChanged(this, previousColumnSource);
}
}
/**
* Returns the editor used to edit rows in this table.
*
* @return
* The row editor, or <tt>null</tt> if no editor is installed.
*/
public RowEditor getRowEditor() {
return rowEditor;
}
/**
* Sets the editor used to edit rows in this table.
*
* @param rowEditor
* The row editor for the list.
*/
public void setRowEditor(RowEditor rowEditor) {
RowEditor previousRowEditor = this.rowEditor;
if (previousRowEditor != rowEditor) {
this.rowEditor = rowEditor;
tableViewListeners.rowEditorChanged(this, previousRowEditor);
}
}
/**
* When in single-select mode, returns the currently selected index.
*
* @return
* The currently selected index.
*/
public int getSelectedIndex() {
if (selectMode != SelectMode.SINGLE) {
throw new IllegalStateException("Table view is not in single-select mode.");
}
return (selectedRanges.getLength() == 0) ? -1 : selectedRanges.get(0).start;
}
/**
* Sets the selection to a single index.
*
* @param index
* The index to select, or <tt>-1</tt> to clear the selection.
*/
public void setSelectedIndex(int index) {
if (index == -1) {
clearSelection();
} else {
setSelectedRange(index, index);
}
}
/**
* Sets the selection to a single range.
*
* @param start
* @param end
*/
public void setSelectedRange(int start, int end) {
ArrayList<Span> selectedRanges = new ArrayList<Span>();
selectedRanges.add(new Span(start, end));
setSelectedRanges(selectedRanges);
}
/**
* Returns the table's current selection.
*/
public Sequence<Span> getSelectedRanges() {
return new ListSelectionSequence(selectedRanges);
}
/**
* Sets the selection to the given range sequence. Any overlapping or
* connecting ranges will be consolidated, and the resulting selection will
* be sorted in ascending order.
*
* @param selectedRanges
*
* @return
* The ranges that were actually set.
*/
public Sequence<Span> setSelectedRanges(Sequence<Span> selectedRanges) {
if (selectedRanges == null) {
throw new IllegalArgumentException("selectedRanges is null.");
}
if (selectMode == SelectMode.NONE) {
throw new IllegalArgumentException("Selection is not enabled.");
}
if (selectMode == SelectMode.SINGLE) {
int n = selectedRanges.getLength();
if (n > 1) {
throw new IllegalArgumentException("Selection length is greater than 1.");
}
if (n > 0) {
Span selectedRange = selectedRanges.get(0);
if (selectedRange.getLength() > 1) {
throw new IllegalArgumentException("Selected range length is greater than 1.");
}
}
}
// Update the selection
ListSelectionSequence previousSelectedRanges = new ListSelectionSequence(this.selectedRanges);
ListSelection listSelection = new ListSelection();
for (int i = 0, n = selectedRanges.getLength(); i < n; i++) {
Span range = selectedRanges.get(i);
if (range == null) {
throw new IllegalArgumentException("range is null.");
}
if (range.start < 0 || range.end >= tableData.getLength()) {
throw new IndexOutOfBoundsException();
}
listSelection.addRange(range.start, range.end);
}
this.selectedRanges = listSelection;
// Notify listeners
tableViewSelectionListeners.selectedRangesChanged(this, previousSelectedRanges);
return getSelectedRanges();
}
/**
* Sets the selection to the given range sequence.
*
* @param selectedRanges
* A JSON-formatted string containing the ranges to select.
*
* @return
* The ranges that were actually set.
*
* @see #setSelectedRanges(Sequence)
*/
public final Sequence<Span> setSelectedRanges(String selectedRanges) {
if (selectedRanges == null) {
throw new IllegalArgumentException("selectedRanges is null.");
}
try {
setSelectedRanges(parseSelectedRanges(selectedRanges));
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
}
return getSelectedRanges();
}
@SuppressWarnings("unchecked")
private Sequence<Span> parseSelectedRanges(String json)
throws SerializationException {
ArrayList<Span> selectedRanges = new ArrayList<Span>();
List<?> list = JSONSerializer.parseList(json);
for (Object item : list) {
Map<String, ?> map = (Map<String, ?>)item;
selectedRanges.add(new Span(map));
}
return selectedRanges;
}
/**
* Returns the first selected index.
*
* @return
* The first selected index, or <tt>-1</tt> if nothing is selected.
*/
public int getFirstSelectedIndex() {
return (selectedRanges.getLength() > 0) ? selectedRanges.get(0).start : -1;
}
/**
* Returns the last selected index.
*
* @return
* The last selected index, or <tt>-1</tt> if nothing is selected.
*/
public int getLastSelectedIndex() {
return (selectedRanges.getLength() > 0) ?
selectedRanges.get(selectedRanges.getLength() - 1).end : -1;
}
/**
* Adds a single index to the selection.
*
* @param index
* The index to add.
*
* @return
* <tt>true</tt> if the index was added to the selection; <tt>false</tt>,
* otherwise.
*/
public boolean addSelectedIndex(int index) {
Sequence<Span> addedRanges = addSelectedRange(index, index);
return (addedRanges.getLength() > 0);
}
/**
* Adds a range of indexes to the selection.
*
* @param start
* The first index in the range.
*
* @param end
* The last index in the range.
*
* @return
* The ranges that were added to the selection.
*/
public Sequence<Span> addSelectedRange(int start, int end) {
if (selectMode != SelectMode.MULTI) {
throw new IllegalStateException("Table view is not in multi-select mode.");
}
if (start < 0 || end >= tableData.getLength()) {
throw new IndexOutOfBoundsException();
}
Sequence<Span> addedRanges = selectedRanges.addRange(start, end);
for (int i = 0, n = addedRanges.getLength(); i < n; i++) {
Span addedRange = addedRanges.get(i);
tableViewSelectionListeners.selectedRangeAdded(this, addedRange.start, addedRange.end);
}
return addedRanges;
}
/**
* Adds a range of indexes to the selection.
*
* @param range
* The range to add.
*
* @return
* The ranges that were added to the selection.
*/
public Sequence<Span> addSelectedRange(Span range) {
if (range == null) {
throw new IllegalArgumentException("range is null.");
}
return addSelectedRange(range.start, range.end);
}
/**
* Removes a single index from the selection.
*
* @param index
* The index to remove.
*
* @return
* <tt>true</tt> if the index was removed from the selection;
* <tt>false</tt>, otherwise.
*/
public boolean removeSelectedIndex(int index) {
Sequence<Span> removedRanges = removeSelectedRange(index, index);
return (removedRanges.getLength() > 0);
}
/**
* Removes a range of indexes from the selection.
*
* @param start
* The start of the range to remove.
*
* @param end
* The end of the range to remove.
*
* @return
* The ranges that were removed from the selection.
*/
public Sequence<Span> removeSelectedRange(int start, int end) {
if (selectMode != SelectMode.MULTI) {
throw new IllegalStateException("Table view is not in multi-select mode.");
}
if (start < 0 || end >= tableData.getLength()) {
throw new IndexOutOfBoundsException();
}
Sequence<Span> removedRanges = selectedRanges.removeRange(start, end);
for (int i = 0, n = removedRanges.getLength(); i < n; i++) {
Span removedRange = removedRanges.get(i);
tableViewSelectionListeners.selectedRangeRemoved(this, removedRange.start, removedRange.end);
}
return removedRanges;
}
/**
* Removes a range of indexes from the selection.
*
* @param range
* The range to remove.
*
* @return
* The ranges that were removed from the selection.
*/
public Sequence<Span> removeSelectedRange(Span range) {
if (range == null) {
throw new IllegalArgumentException("range is null.");
}
return removeSelectedRange(range.start, range.end);
}
/**
* Selects all rows in the table.
*/
public void selectAll() {
setSelectedRange(0, tableData.getLength() - 1);
}
/**
* Clears the selection.
*/
public void clearSelection() {
if (selectedRanges.getLength() > 0) {
Sequence<Span> previousSelectedRanges = new ListSelectionSequence(selectedRanges);
selectedRanges = new ListSelection();
tableViewSelectionListeners.selectedRangesChanged(this,
previousSelectedRanges);
}
}
/**
* Returns the selection state of a given index.
*
* @param index
* The index whose selection state is to be tested.
*
* @return <tt>true</tt> if the index is selected; <tt>false</tt>,
* otherwise.
*/
public boolean isRowSelected(int index) {
if (index < 0 || index >= tableData.getLength()) {
throw new IndexOutOfBoundsException();
}
return (selectedRanges.containsIndex(index));
}
public Object getSelectedRow() {
int index = getSelectedIndex();
Object row = null;
if (index >= 0) {
row = tableData.get(index);
}
return row;
}
public Sequence<?> getSelectedRows() {
ArrayList<Object> rows = new ArrayList<Object>();
for (int i = 0, n = selectedRanges.getLength(); i < n; i++) {
Span range = selectedRanges.get(i);
for (int index = range.start; index <= range.end; index++) {
Object row = tableData.get(index);
rows.add(row);
}
}
return rows;
}
/**
* Returns the current selection mode.
*/
public SelectMode getSelectMode() {
return selectMode;
}
/**
* Sets the selection mode. Clears the selection if the mode has changed.
*
* @param selectMode
* The new selection mode.
*/
public void setSelectMode(SelectMode selectMode) {
if (selectMode == null) {
throw new IllegalArgumentException("selectMode is null.");
}
SelectMode previousSelectMode = this.selectMode;
if (previousSelectMode != selectMode) {
// Clear any current selection
clearSelection();
// Update the selection mode
this.selectMode = selectMode;
// Fire select mode change event
tableViewListeners.selectModeChanged(this, previousSelectMode);
}
}
public void setSelectMode(String selectMode) {
if (selectMode == null) {
throw new IllegalArgumentException("selectMode is null.");
}
setSelectMode(SelectMode.valueOf(selectMode.toUpperCase()));
}
/**
* Returns the table view's sort dictionary.
*/
public SortDictionary getSort() {
return sortDictionary;
}
/**
* Sets the table view's sort.
*
* @param columnName
* @param sortDirection
*/
@SuppressWarnings("unchecked")
public Dictionary<String, SortDirection> setSort(String columnName, SortDirection sortDirection) {
Dictionary.Pair<String, SortDirection> sort =
new Dictionary.Pair<String, SortDirection>(columnName, sortDirection);
setSort(new ArrayList<Dictionary.Pair<String, SortDirection>>(sort));
return getSort();
}
/**
* Sets the table view's sort.
*
* @param sort
* A sequence of key/value pairs representing the sort. Keys represent column names and
* values represent sort direction.
*/
public Dictionary<String, SortDirection> setSort(Sequence<Dictionary.Pair<String, SortDirection>> sort) {
if (sort == null) {
throw new IllegalArgumentException();
}
sortMap.clear();
sortList.clear();
for (int i = 0, n = sort.getLength(); i < n; i++) {
Dictionary.Pair<String, SortDirection> pair = sort.get(i);
if (!sortMap.containsKey(pair.key)) {
sortMap.put(pair.key, pair.value);
sortList.add(pair.key);
}
}
tableViewSortListeners.sortChanged(this);
return getSort();
}
/**
* Sets the table view's sort.
*
* @param sort
* A JSON list containing JSON objects representing the sort.
*
* @see #setSort(Sequence)
*/
public final Dictionary<String, SortDirection> setSort(String sort) {
if (sort == null) {
throw new IllegalArgumentException();
}
try {
setSort(parseSort(sort));
} catch (SerializationException exception) {
throw new IllegalArgumentException(exception);
}
return getSort();
}
@SuppressWarnings("unchecked")
private Sequence<Dictionary.Pair<String, SortDirection>> parseSort(String json)
throws SerializationException {
ArrayList<Dictionary.Pair<String, SortDirection>> sort =
new ArrayList<Dictionary.Pair<String, SortDirection>>();
List<?> list = JSONSerializer.parseList(json);
for (Object item : list) {
Map<String, ?> map = (Map<String, ?>)item;
Dictionary.Pair<String, SortDirection> pair =
new Dictionary.Pair<String, SortDirection>((String)map.get(COLUMN_NAME_KEY),
SortDirection.valueOf(((String)map.get(SORT_DIRECTION_KEY)).toLowerCase()));
sort.add(pair);
}
return sort;
}
/**
* Clears the sort.
*/
public void clearSort() {
if (!sortMap.isEmpty()) {
sortMap.clear();
sortList.clear();
tableViewSortListeners.sortChanged(this);
}
}
/**
* Returns the disabled state of a given row.
*
* @param index
* The index of the row whose disabled state is to be tested.
*
* @return
* <tt>true</tt> if the row is disabled; <tt>false</tt>,
* otherwise.
*/
@SuppressWarnings("unchecked")
public boolean isRowDisabled(int index) {
boolean disabled = false;
if (disabledRowFilter != null) {
Object row = tableData.get(index);
disabled = ((Filter<Object>)disabledRowFilter).include(row);
}
return disabled;
}
/**
* Returns the disabled row filter.
*
* @return
* The disabled row filter, or <tt>null</tt> if no disabled row filter is
* set.
*/
public Filter<?> getDisabledRowFilter() {
return disabledRowFilter;
}
/**
* Sets the disabled row filter.
*
* @param disabledRowFilter
* The disabled row filter, or <tt>null</tt> for no disabled row filter.
*/
public void setDisabledRowFilter(Filter<?> disabledRowFilter) {
Filter<?> previousDisabledRowFilter = this.disabledRowFilter;
if (previousDisabledRowFilter != disabledRowFilter) {
this.disabledRowFilter = disabledRowFilter;
tableViewListeners.disabledRowFilterChanged(this, previousDisabledRowFilter);
}
}
/**
* Returns the index of the row at a given location.
*
* @param y
* The y-coordinate of the row to identify.
*
* @return
* The row index, or <tt>-1</tt> if there is no row at the given
* y-coordinate.
*/
public int getRowAt(int y) {
TableView.Skin tableViewSkin = (TableView.Skin)getSkin();
return tableViewSkin.getRowAt(y);
}
/**
* Returns the index of the column at a given location.
*
* @param x
* The x-coordinate of the column to identify.
*
* @return
* The column index, or <tt>-1</tt> if there is no column at the given
* x-coordinate.
*/
public int getColumnAt(int x) {
TableView.Skin tableViewSkin = (TableView.Skin)getSkin();
return tableViewSkin.getColumnAt(x);
}
/**
* Returns the bounding area of a given row.
*
* @param rowIndex
* The row index.
*
* @return
* The bounding area of the row.
*/
public Bounds getRowBounds(int rowIndex) {
TableView.Skin tableViewSkin = (TableView.Skin)getSkin();
return tableViewSkin.getRowBounds(rowIndex);
}
/**
* Returns the bounding area of a given column.
*
* @param columnIndex
* The column index.
*
* @return
* The bounding area of the column.
*/
public Bounds getColumnBounds(int columnIndex) {
TableView.Skin tableViewSkin = (TableView.Skin)getSkin();
return tableViewSkin.getColumnBounds(columnIndex);
}
/**
* Returns the bounding area of a given cell.
*
* @param rowIndex
* The row index of the cell.
*
* @param columnIndex
* The column index of the cell.
*
* @return
* The bounding area of the cell.
*/
public Bounds getCellBounds(int rowIndex, int columnIndex) {
TableView.Skin tableViewSkin = (TableView.Skin)getSkin();
return tableViewSkin.getCellBounds(rowIndex, columnIndex);
}
public ListenerList<TableViewListener> getTableViewListeners() {
return tableViewListeners;
}
public ListenerList<TableViewColumnListener> getTableViewColumnListeners() {
return tableViewColumnListeners;
}
public ListenerList<TableViewRowListener> getTableViewRowListeners() {
return tableViewRowListeners;
}
public ListenerList<TableViewSelectionListener> getTableViewSelectionListeners() {
return tableViewSelectionListeners;
}
public ListenerList<TableViewSortListener> getTableViewSortListeners() {
return tableViewSortListeners;
}
}
|