blob: b2c6e0b7a3c40ca56fca1ef8b2c0544e274b36df [file] [log] [blame]
/*
* 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.util.Iterator;
import org.apache.pivot.beans.DefaultProperty;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.util.ImmutableIterator;
import org.apache.pivot.util.ListenerList;
/**
* Container that arranges components in a two-dimensional grid, optionally
* spanning multiple rows and columns, much like an HTML <tt>&lt;table&gt;</tt>
* element. <p> Note that unlike an HTML <tt>&lt;table&gt;</tt>, components that
* span multiple rows or columns will not "push" other components out of their
* way. Instead, the spanning components will simply overlay the cells into
* which they span. This means that application developers may have to use
* {@link Filler filler cells} in the cells that are spanned.
*/
@DefaultProperty("rows")
public class TablePane extends Container {
/**
* Represents a table pane row.
*/
public static class Row implements Sequence<Component>, Iterable<Component> {
private int height;
private boolean relative;
private boolean highlighted;
private ArrayList<Component> cells = new ArrayList<>();
private TablePane tablePane = null;
public Row() {
this(null, -1, false, false);
}
public Row(int height) {
this(null, height, false, false);
}
public Row(int height, boolean relative) {
this(null, height, relative, false);
}
public Row(int height, boolean relative, boolean highlighted) {
this(null, height, relative, highlighted);
}
public Row(TablePane tablePane) {
this(tablePane, -1, false, false);
}
public Row(TablePane tablePane, int height) {
this(tablePane, height, false, false);
}
public Row(TablePane tablePane, int height, boolean relative) {
this(tablePane, height, relative, false);
}
public Row(TablePane tablePane, int height, boolean relative, boolean highlighted) {
this.height = height;
this.relative = relative;
this.highlighted = highlighted;
if (tablePane != null) {
tablePane.getRows().add(this);
}
}
/**
* Returns the table pane with which this row is associated.
*
* @return The row's table pane, or <tt>null</tt> if the row does not
* currently belong to a table.
*/
public TablePane getTablePane() {
return tablePane;
}
/**
* Returns the row height.
*
* @return The height of the row.
*/
public int getHeight() {
return height;
}
/**
* Returns the relative flag.
*
* @return <tt>true</tt> if the row height is relative, <tt>false</tt>
* if it is fixed.
*/
public boolean isRelative() {
return relative;
}
/**
* Set the row height.
*
* @param height The absolute height of the row.
*/
public void setHeight(int height) {
setHeight(height, false);
}
/**
* Set the row height.
*
* @param height The encoded height 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 setHeight(String height) {
boolean relativeLocal = false;
if (height.endsWith(RELATIVE_SIZE_INDICATOR)) {
relativeLocal = true;
setHeight(Integer.parseInt(height.substring(0, height.length() - 1)), relativeLocal);
} else {
setHeight(Integer.parseInt(height), relativeLocal);
}
}
/**
* Sets the row height.
*
* @param height The height of the row.
* @param relative <tt>true</tt> if the row height is relative,
* <tt>false</tt> if it is fixed.
*/
public void setHeight(int height, boolean relative) {
int previousHeight = this.height;
boolean previousRelative = this.relative;
if (previousHeight != height || previousRelative != relative) {
this.height = height;
this.relative = relative;
if (tablePane != null) {
tablePane.tablePaneListeners.rowHeightChanged(this, previousHeight,
previousRelative);
}
}
}
/**
* Returns the highlighted flag.
*
* @return <tt>true</tt> if the row is highlighted, <tt>false</tt> if it
* is not
*/
public boolean isHighlighted() {
return highlighted;
}
/**
* Sets the highlighted flag.
*
* @param highlighted <tt>true</tt> to set the row as highlighted,
* <tt>false</tt> to set it as not highlighted
*/
public void setHighlighted(boolean highlighted) {
if (highlighted != this.highlighted) {
this.highlighted = highlighted;
if (tablePane != null) {
tablePane.tablePaneListeners.rowHighlightedChanged(this);
}
}
}
@Override
public int add(Component component) {
int index = getLength();
insert(component, index);
return index;
}
@Override
public void insert(Component component, int index) {
if (component == null) {
throw new IllegalArgumentException("Component is null.");
}
if (component.getParent() != null) {
throw new IllegalArgumentException("Component already has a parent.");
}
cells.insert(component, index);
if (tablePane != null) {
tablePane.add(component);
tablePane.tablePaneListeners.cellInserted(this, index);
}
}
@Override
public Component update(int index, Component component) {
Component previousComponent = cells.get(index);
if (component != previousComponent) {
if (component == null) {
throw new IllegalArgumentException("Component is null.");
}
if (component.getParent() != null) {
throw new IllegalArgumentException("Component already has a parent.");
}
cells.update(index, component);
if (tablePane != null) {
tablePane.add(component);
tablePane.tablePaneListeners.cellUpdated(this, index, previousComponent);
tablePane.remove(previousComponent);
}
}
return previousComponent;
}
@Override
public int remove(Component component) {
int index = indexOf(component);
if (index != -1) {
remove(index, 1);
}
return index;
}
@Override
public Sequence<Component> remove(int index, int count) {
Sequence<Component> removed = cells.remove(index, count);
if (tablePane != null) {
tablePane.tablePaneListeners.cellsRemoved(this, index, removed);
for (int i = 0, n = removed.getLength(); i < n; i++) {
Component component = removed.get(i);
tablePane.remove(component);
}
}
return removed;
}
@Override
public Component get(int index) {
return cells.get(index);
}
@Override
public int indexOf(Component component) {
return cells.indexOf(component);
}
@Override
public int getLength() {
return cells.getLength();
}
@Override
public Iterator<Component> iterator() {
return new ImmutableIterator<>(cells.iterator());
}
}
/**
* Represents a table pane column.
*/
public static class Column {
private TablePane tablePane = null;
private int width;
private boolean relative;
private boolean highlighted;
public Column() {
this(null, -1, false, false);
}
public Column(int width) {
this(null, width, false, false);
}
public Column(int width, boolean relative) {
this(null, width, relative, false);
}
public Column(int width, boolean relative, boolean highlighted) {
this(null, width, relative, highlighted);
}
public Column(TablePane tablePane) {
this(tablePane, -1, false, false);
}
public Column(TablePane tablePane, int width) {
this(tablePane, width, false, false);
}
public Column(TablePane tablePane, int width, boolean relative) {
this(tablePane, width, relative, false);
}
public Column(TablePane tablePane, int width, boolean relative, boolean highlighted) {
this.width = width;
this.relative = relative;
this.highlighted = highlighted;
if (tablePane != null) {
tablePane.getColumns().add(this);
}
}
/**
* Returns the table pane with which this column is associated.
*
* @return The column's table pane, or <tt>null</tt> if the column does
* not currently belong to a table.
*/
public TablePane getTablePane() {
return tablePane;
}
/**
* 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 relativeLocal = false;
if (width.endsWith(RELATIVE_SIZE_INDICATOR)) {
relativeLocal = true;
setWidth(Integer.parseInt(width.substring(0, width.length() - 1)), relativeLocal);
} else {
setWidth(Integer.parseInt(width), relativeLocal);
}
}
/**
* 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) {
int previousWidth = this.width;
boolean previousRelative = this.relative;
if (previousWidth != width || previousRelative != relative) {
this.width = width;
this.relative = relative;
if (tablePane != null) {
tablePane.tablePaneListeners.columnWidthChanged(this, previousWidth,
previousRelative);
}
}
}
/**
* Returns the highlighted flag.
*
* @return <tt>true</tt> if the column is highlighted, <tt>false</tt> if
* it is not
*/
public boolean isHighlighted() {
return highlighted;
}
/**
* Sets the highlighted flag.
*
* @param highlighted <tt>true</tt> to set the column as highlighted,
* <tt>false</tt> to set it as not highlighted
*/
public void setHighlighted(boolean highlighted) {
if (highlighted != this.highlighted) {
this.highlighted = highlighted;
if (tablePane != null) {
tablePane.tablePaneListeners.columnHighlightedChanged(this);
}
}
}
}
/**
* Table pane skin interface. Table pane skins must implement this interface
* to facilitate additional communication between the component and the
* skin.
*/
public interface Skin {
public int getRowAt(int y);
public Bounds getRowBounds(int row);
public int getColumnAt(int x);
public Bounds getColumnBounds(int column);
}
/**
* Class that manages a table pane's row list. Callers get access to the row
* sequence via {@link TablePane#getRows()}.
*/
public final class RowSequence implements Sequence<Row>, Iterable<Row> {
private RowSequence() {
}
@Override
public int add(Row row) {
int index = getLength();
insert(row, index);
return index;
}
@Override
public void insert(Row row, int index) {
if (row == null) {
throw new IllegalArgumentException("row is null.");
}
if (row.tablePane != null) {
throw new IllegalArgumentException("row is already in use by another table pane.");
}
rows.insert(row, index);
row.tablePane = TablePane.this;
for (int i = 0, n = row.getLength(); i < n; i++) {
Component component = row.get(i);
TablePane.this.add(component);
}
// Notify listeners
tablePaneListeners.rowInserted(TablePane.this, index);
}
@Override
public Row update(int index, Row row) {
throw new UnsupportedOperationException();
}
@Override
public int remove(Row row) {
int index = indexOf(row);
if (index != -1) {
remove(index, 1);
}
return index;
}
@Override
public Sequence<Row> remove(int index, int count) {
Sequence<Row> removed = rows.remove(index, count);
if (count > 0) {
for (int i = 0, n = removed.getLength(); i < n; i++) {
Row row = removed.get(i);
row.tablePane = null;
for (int j = 0, m = row.getLength(); j < m; j++) {
Component component = row.get(j);
TablePane.this.remove(component);
}
}
tablePaneListeners.rowsRemoved(TablePane.this, index, removed);
}
return removed;
}
@Override
public Row get(int index) {
return rows.get(index);
}
@Override
public int indexOf(Row row) {
return rows.indexOf(row);
}
@Override
public int getLength() {
return rows.getLength();
}
@Override
public Iterator<Row> iterator() {
return new ImmutableIterator<>(rows.iterator());
}
}
/**
* Class that manages a table pane's column list. Callers get access to the
* column sequence via {@link TablePane#getColumns()}.
*/
public final class ColumnSequence implements Sequence<Column>, Iterable<Column> {
private ColumnSequence() {
}
@Override
public int add(Column column) {
int index = getLength();
insert(column, index);
return index;
}
@Override
public void insert(Column column, int index) {
if (column == null) {
throw new IllegalArgumentException("column is null.");
}
if (column.tablePane != null) {
throw new IllegalArgumentException(
"column is already in use by another table pane.");
}
columns.insert(column, index);
column.tablePane = TablePane.this;
// Notify listeners
tablePaneListeners.columnInserted(TablePane.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++) {
Column column = removed.get(i);
column.tablePane = null;
}
tablePaneListeners.columnsRemoved(TablePane.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<>(columns.iterator());
}
}
/**
* Component that can be used as filler for empty cells.
*/
public static final class Filler extends Component {
public Filler() {
installSkin(Filler.class);
}
}
private enum Attribute {
ROW_SPAN, COLUMN_SPAN;
}
private static class TablePaneListenerList extends WTKListenerList<TablePaneListener> implements
TablePaneListener {
@Override
public void rowInserted(TablePane tablePane, int index) {
for (TablePaneListener listener : this) {
listener.rowInserted(tablePane, index);
}
}
@Override
public void rowsRemoved(TablePane tablePane, int index, Sequence<TablePane.Row> rows) {
for (TablePaneListener listener : this) {
listener.rowsRemoved(tablePane, index, rows);
}
}
@Override
public void rowHeightChanged(TablePane.Row row, int previousHeight, boolean previousRelative) {
for (TablePaneListener listener : this) {
listener.rowHeightChanged(row, previousHeight, previousRelative);
}
}
@Override
public void rowHighlightedChanged(TablePane.Row row) {
for (TablePaneListener listener : this) {
listener.rowHighlightedChanged(row);
}
}
@Override
public void columnInserted(TablePane tablePane, int index) {
for (TablePaneListener listener : this) {
listener.columnInserted(tablePane, index);
}
}
@Override
public void columnsRemoved(TablePane tablePane, int index,
Sequence<TablePane.Column> columns) {
for (TablePaneListener listener : this) {
listener.columnsRemoved(tablePane, index, columns);
}
}
@Override
public void columnWidthChanged(TablePane.Column column, int previousWidth,
boolean previousRelative) {
for (TablePaneListener listener : this) {
listener.columnWidthChanged(column, previousWidth, previousRelative);
}
}
@Override
public void columnHighlightedChanged(TablePane.Column column) {
for (TablePaneListener listener : this) {
listener.columnHighlightedChanged(column);
}
}
@Override
public void cellInserted(TablePane.Row row, int column) {
for (TablePaneListener listener : this) {
listener.cellInserted(row, column);
}
}
@Override
public void cellsRemoved(TablePane.Row row, int column, Sequence<Component> removed) {
for (TablePaneListener listener : this) {
listener.cellsRemoved(row, column, removed);
}
}
@Override
public void cellUpdated(TablePane.Row row, int column, Component previousComponent) {
for (TablePaneListener listener : this) {
listener.cellUpdated(row, column, previousComponent);
}
}
}
private static class TablePaneAttributeListenerList extends
WTKListenerList<TablePaneAttributeListener> implements TablePaneAttributeListener {
@Override
public void rowSpanChanged(TablePane tablePane, Component component, int previousRowSpan) {
for (TablePaneAttributeListener listener : this) {
listener.rowSpanChanged(tablePane, component, previousRowSpan);
}
}
@Override
public void columnSpanChanged(TablePane tablePane, Component component,
int previousColumnSpan) {
for (TablePaneAttributeListener listener : this) {
listener.columnSpanChanged(tablePane, component, previousColumnSpan);
}
}
}
private ArrayList<Row> rows = null;
private RowSequence rowSequence = new RowSequence();
private ArrayList<Column> columns = null;
private ColumnSequence columnSequence = new ColumnSequence();
private TablePaneListenerList tablePaneListeners = new TablePaneListenerList();
private TablePaneAttributeListenerList tablePaneAttributeListeners = new TablePaneAttributeListenerList();
public static final String RELATIVE_SIZE_INDICATOR = "*";
/**
* Creates a new <tt>TablePane</tt> with empty row and column sequences.
*/
public TablePane() {
this(new ArrayList<Column>());
}
/**
* Creates a new <tt>TablePane</tt> with the specified columns.
*
* @param columns The column sequence to use. A copy of this sequence will
* be made
*/
public TablePane(Sequence<Column> columns) {
if (columns == null) {
throw new IllegalArgumentException("columns is null");
}
this.rows = new ArrayList<>();
this.columns = new ArrayList<>(columns);
installSkin(TablePane.class);
}
@Override
protected void setSkin(org.apache.pivot.wtk.Skin skin) {
if (!(skin instanceof TablePane.Skin)) {
throw new IllegalArgumentException("Skin class must implement "
+ TablePane.Skin.class.getName());
}
super.setSkin(skin);
}
/**
* Returns the table pane row sequence.
*
* @return The table pane row sequence
*/
public RowSequence getRows() {
return rowSequence;
}
/**
* 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) {
TablePane.Skin tablePaneSkin = (TablePane.Skin) getSkin();
return tablePaneSkin.getRowAt(y);
}
/**
* @return The bounds of a given row.
*
* @param row The row index.
*/
public Bounds getRowBounds(int row) {
TablePane.Skin tablePaneSkin = (TablePane.Skin) getSkin();
return tablePaneSkin.getRowBounds(row);
}
/**
* @return The table pane column sequence
*/
public ColumnSequence getColumns() {
return columnSequence;
}
/**
* 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) {
TablePane.Skin tablePaneSkin = (TablePane.Skin) getSkin();
return tablePaneSkin.getColumnAt(x);
}
/**
* @return The bounds of a given column.
*
* @param column The column index.
*/
public Bounds getColumnBounds(int column) {
TablePane.Skin tablePaneSkin = (TablePane.Skin) getSkin();
return tablePaneSkin.getColumnBounds(column);
}
/**
* Gets the component at the specified cell in this table pane.
*
* @param rowIndex The row index of the cell
* @param columnIndex The column index of the cell
* @return The component in the specified cell, or <tt>null</tt> if the cell
* is empty
*/
public Component getCellComponent(int rowIndex, int columnIndex) {
Row row = rows.get(rowIndex);
Component component = null;
if (row.getLength() > columnIndex) {
component = row.get(columnIndex);
}
return component;
}
/**
* Overrides the base method to check whether or not a cell component is
* being removed, and fires the appropriate event in that case.
*
* @param index The index at which components were removed
* @param count The number of components removed
* @return The sequence of components that were removed
*/
@Override
public Sequence<Component> remove(int index, int count) {
for (int i = index, n = index + count; i < n; i++) {
Component component = get(i);
for (Row row : rows) {
if (row.indexOf(component) >= 0) {
throw new UnsupportedOperationException();
}
}
}
// Call the base method to remove the components
return super.remove(index, count);
}
/**
* @return The table pane listener list.
*/
public ListenerList<TablePaneListener> getTablePaneListeners() {
return tablePaneListeners;
}
/**
* @return The table pane attribute listener list.
*/
public ListenerList<TablePaneAttributeListener> getTablePaneAttributeListeners() {
return tablePaneAttributeListeners;
}
public static int getRowSpan(Component component) {
Integer value = (Integer) component.getAttribute(Attribute.ROW_SPAN);
return (value == null) ? 1 : value.intValue();
}
public static void setRowSpan(Component component, int rowSpan) {
Integer previousValue = (Integer) component.setAttribute(Attribute.ROW_SPAN, Integer.valueOf(rowSpan));
int previousRowSpan = (previousValue == null) ? 1 : previousValue.intValue();
if (previousRowSpan != rowSpan) {
Container parent = component.getParent();
if (parent instanceof TablePane) {
TablePane tablePane = (TablePane) parent;
tablePane.tablePaneAttributeListeners.rowSpanChanged(tablePane, component,
previousRowSpan);
}
}
}
public static int getColumnSpan(Component component) {
Integer value = (Integer) component.getAttribute(Attribute.COLUMN_SPAN);
return (value == null) ? 1 : value.intValue();
}
public static void setColumnSpan(Component component, int columnSpan) {
Integer previousValue = (Integer) component.setAttribute(Attribute.COLUMN_SPAN, Integer.valueOf(columnSpan));
int previousColumnSpan = (previousValue == null) ? 1 : previousValue.intValue();
if (previousColumnSpan != columnSpan) {
Container parent = component.getParent();
if (parent instanceof TablePane) {
TablePane tablePane = (TablePane) parent;
tablePane.tablePaneAttributeListeners.columnSpanChanged(tablePane, component,
previousColumnSpan);
}
}
}
}