/******************************************************************************* * Copyright (c) 2013, 2016 Dirk Fauth and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Dirk Fauth <dirk.fauth@gmail.com> - initial API and implementation * neal zhang <nujiah001@126.com> - Bug 442009 *******************************************************************************/ package org.eclipse.nebula.widgets.nattable.data; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; import java.util.StringTokenizer; import org.eclipse.nebula.widgets.nattable.layer.cell.DataCell; import org.eclipse.nebula.widgets.nattable.persistence.IPersistable; /** * This implementation of ISpanningDataProvider will automatically span cells if * the containing cell values are equal. It supports configuration whether the * automatic spanning should be performed for columns or cells. It is even * possible to configure which columns/rows should be checked for auto spanning. * <p> * It wraps the IDataProvider that is used for providing the data to the * NatTable, so it is possible to use existing code and enhance it easily with * the auto spanning feature. * <p> * To use the auto spanning feature you simply need to exchange the DataLayer in * your layer composition with the SpanningDataLayer and wrap the exising * IDataProvider with this AutomaticSpanningDataProvider. * <p> * <b>Note: </b><br> * Mixing of automatic column and row spanning could cause several rendering * issues if there can be no rectangle build out of matching cell values. If a * mixing is needed, a more complicated calculation algorithm need to be * implemented that checks every columns and row by building the spanning cell * for the matching rectangle. As this would be quite time consuming * calculations, this is not supported out of the box by NatTable. */ public class AutomaticSpanningDataProvider implements ISpanningDataProvider, IPersistable { public static final String PERSISTENCE_KEY_AUTO_COLUMN_SPAN = ".autoColumnSpan"; //$NON-NLS-1$ public static final String PERSISTENCE_KEY_AUTO_ROW_SPAN = ".autoRowSpan"; //$NON-NLS-1$ public static final String PERSISTENCE_KEY_AUTO_SPAN_COLUMNS = ".autoSpanColumns"; //$NON-NLS-1$ public static final String PERSISTENCE_KEY_AUTO_SPAN_ROWS = ".autoSpanRows"; //$NON-NLS-1$ /** * The IDataProvider that is wrapped by this AutomaticSpanningDataProvider */ private final IDataProvider underlyingDataProvider; /** * Flag to configure this AutomaticSpanningDataProvider to perform automatic * column spanning */ private boolean autoColumnSpan; /** * Flag to configure this AutomaticSpanningDataProvider to perform automatic * row spanning */ private boolean autoRowSpan; /** * List of column positions for which automatic spanning is enabled. * <p> * <b>Note: </b>If this list is empty, all columns will do auto row * spanning. */ private List<Integer> autoSpanColumns = new ArrayList<Integer>(); /** * List of row positions for which automatic spanning is enabled. * <p> * <b>Note: </b>If this list is empty, all rows will do auto column * spanning. */ private List<Integer> autoSpanRows = new ArrayList<Integer>(); /** * * @param underlyingDataProvider * The IDataProvider that should be wrapped by this * AutomaticSpanningDataProvider * @param autoColumnSpan * Flag to configure this AutomaticSpanningDataProvider to * perform automatic column spanning * @param autoRowSpan * Flag to configure this AutomaticSpanningDataProvider to * perform automatic row spanning */ public AutomaticSpanningDataProvider( IDataProvider underlyingDataProvider, boolean autoColumnSpan, boolean autoRowSpan) { this.underlyingDataProvider = underlyingDataProvider; this.autoColumnSpan = autoColumnSpan; this.autoRowSpan = autoRowSpan; } @Override public Object getDataValue(int columnIndex, int rowIndex) { return this.underlyingDataProvider.getDataValue(columnIndex, rowIndex); } @Override public void setDataValue(int columnIndex, int rowIndex, Object newValue) { this.underlyingDataProvider.setDataValue(columnIndex, rowIndex, newValue); } @Override public int getColumnCount() { return this.underlyingDataProvider.getColumnCount(); } @Override public int getRowCount() { return this.underlyingDataProvider.getRowCount(); } @Override public DataCell getCellByPosition(int columnPosition, int rowPosition) { int cellColumnPosition = isAutoSpanEnabledForColumn(columnPosition, rowPosition) ? getStartColumnPosition(columnPosition, rowPosition) : columnPosition; int cellRowPosition = isAutoSpanEnabledForRow(columnPosition, rowPosition) ? getStartRowPosition(columnPosition, rowPosition) : rowPosition; int columnSpan = isAutoSpanEnabledForColumn(columnPosition, rowPosition) ? getColumnSpan(cellColumnPosition, cellRowPosition) : 1; int rowSpan = isAutoSpanEnabledForRow(columnPosition, rowPosition) ? getRowSpan(cellColumnPosition, cellRowPosition) : 1; return new DataCell(cellColumnPosition, cellRowPosition, columnSpan, rowSpan); } /** * Check if the given column should be used for auto spanning. * * @param columnPosition * The column position to check for auto spanning * @param rowPosition * The row position for which the column spanning should be * checked * @return <code>true</code> if for that column position auto spanning is * enabled */ protected boolean isAutoSpanEnabledForColumn(int columnPosition, int rowPosition) { return (this.autoColumnSpan && isAutoSpanRow(rowPosition)); } /** * Check if the given row should be used for auto spanning. * * @param columnPosition * The column position for which the row spanning should be * checked. * @param rowPosition * The row position to check for auto spanning * @return <code>true</code> if for that row position auto spanning is * enabled */ protected boolean isAutoSpanEnabledForRow(int columnPosition, int rowPosition) { return (this.autoRowSpan && isAutoSpanColumn(columnPosition)); } /** * Checks if the given column position is configured as a auto span column. * * @param columnPosition * The column position to check * @return <code>true</code> if the given column position is configured as a * auto span column. */ private boolean isAutoSpanColumn(int columnPosition) { return (this.autoSpanColumns.isEmpty() || this.autoSpanColumns.contains(columnPosition)); } /** * Checks if the given row position is configured as a auto span row. * * @param rowPosition * The row position to check * @return <code>true</code> if the given row position is configured as a * auto span row. */ private boolean isAutoSpanRow(int rowPosition) { return (this.autoSpanRows.isEmpty() || this.autoSpanRows.contains(rowPosition)); } /** * Configures the given column positions for auto spanning. This means that * the rows in the given columns will be automatically spanned if the * content is equal. Setting column positions for auto spanning will cause * that the rows in all other columns won't be auto spanned anymore. * * @param columnPositions * The column positions to add for auto spanning. */ public void addAutoSpanningColumnPositions(Integer... columnPositions) { this.autoSpanColumns.addAll(Arrays.asList(columnPositions)); } /** * Configures the given row positions for auto spanning. This means that the * columns in the given rows will be automatically spanned if the content is * equal. Setting row positions for auto spanning will cause that the * columns in all other rows won't be auto spanned anymore. * * @param rowPositions * The row positions to add for auto spanning. */ public void addAutoSpanningRowPositions(Integer... rowPositions) { this.autoSpanRows.addAll(Arrays.asList(rowPositions)); } /** * Removes the given column positions for auto spanning. * * @param columnPositions * The column positions to remove for auto spanning. */ public void removeAutoSpanningColumnPositions(Integer... columnPositions) { this.autoSpanColumns.removeAll(Arrays.asList(columnPositions)); } /** * Removes the given row positions for auto spanning. * * @param rowPositions * The row positions to remove for auto spanning. */ public void removeAutoSpanningRowPositions(Integer... rowPositions) { this.autoSpanRows.removeAll(Arrays.asList(rowPositions)); } /** * Clears the list of column positions for which auto spanning rows is * enabled. Note that clearing the list and leaving the autoRowSpan flag set * to <code>true</code> will cause that on all columns the row spanning will * be performed. */ public void clearAutoSpanningColumnPositions() { this.autoSpanColumns.clear(); } /** * Clears the list of row positions for which auto spanning columns is * enabled. Note that clearing the list and leaving the autoColumnSpan flag * set to <code>true</code> will cause that on all rows the column spanning * will be performed. */ public void clearAutoSpanningRowPositions() { this.autoSpanRows.clear(); } /** * Checks if the column to the left of the given column position contains * the same value. In this case the given column is spanned with the one to * the left and therefore that column position will be returned here. * * @param columnPosition * The column position whose spanning starting column is searched * @param rowPosition * The row position where the column spanning should be * performed. * @return The column position where the spanning starts or the given column * position if it is not spanned with the columns to the left. */ protected int getStartColumnPosition(int columnPosition, int rowPosition) { int columnPos; for (columnPos = columnPosition; columnPos >= 0; columnPos--) { if (columnPos <= 0 || !isAutoSpanColumn(columnPos) || !isAutoSpanColumn(columnPos - 1)) { break; } // get value for the given column Object current = getDataValue(columnPos, rowPosition); // get value of the column to the left Object before = getDataValue(columnPos - 1, rowPosition); if (valuesNotEqual(current, before)) { // the both values are not equal, therefore return the given // column position break; } } return columnPos; } /** * Checks if the row above the given row position contains the same value. * In this case the given row is spanned with the above and therefore the * above row position will be returned here. * * @param columnPosition * The column position for which the row spanning should be * checked * @param rowPosition * The row position whose spanning state should be checked. * @return The row position where the spanning starts or the given row * position if it is not spanned with rows above. */ protected int getStartRowPosition(int columnPosition, int rowPosition) { int rowPos; for (rowPos = rowPosition; rowPos >= 0; rowPos--) { if (rowPos <= 0 || !isAutoSpanRow(rowPos) || !isAutoSpanRow(rowPos - 1)) { break; } // get value of given row Object current = getDataValue(columnPosition, rowPos); // get value of row before Object before = getDataValue(columnPosition, rowPos - 1); if (valuesNotEqual(current, before)) { // the both values are not equal, therefore return the given row break; } } return rowPos; } /** * Calculates the number of columns to span regarding the data of the cells. * * @param columnPosition * The column position to start the check for spanning * @param rowPosition * The row position for which the column spanning should be * checked * @return The number of columns to span */ protected int getColumnSpan(int columnPosition, int rowPosition) { int span = 1; while (columnPosition < getColumnCount() - 1 && isAutoSpanColumn(columnPosition) && isAutoSpanColumn(columnPosition + 1) && !valuesNotEqual( getDataValue(columnPosition, rowPosition), getDataValue(columnPosition + 1, rowPosition))) { span++; columnPosition++; } return span; } /** * Calculates the number of rows to span regarding the data of the cells. * * @param columnPosition * The column position for which the row spanning should be * checked * @param rowPosition * The row position to start the check for spanning * @return The number of rows to span */ protected int getRowSpan(int columnPosition, int rowPosition) { int span = 1; while (rowPosition < getRowCount() - 1 && isAutoSpanRow(rowPosition) && isAutoSpanRow(rowPosition + 1) && !valuesNotEqual( getDataValue(columnPosition, rowPosition), getDataValue(columnPosition, rowPosition + 1))) { span++; rowPosition++; } return span; } /** * Check if the given values are equal. This method is <code>null</code> * sage. * * @param value1 * The first value to check for equality with the second value * @param value2 * The second value to check for equality with the first value. * @return <code>true</code> if the given values are not equal. */ protected boolean valuesNotEqual(Object value1, Object value2) { if (value1 == value2) { return false; } return ((value1 == null && value2 != null) || (value1 != null && value2 == null) || !value1.equals(value2)); } /** * @return <code>true</code> if automatic column spanning is enabled */ public boolean isAutoColumnSpan() { return this.autoColumnSpan; } /** * @param autoColumnSpan * <code>true</code> to enable automatic column spanning, * <code>false</code> to disable it */ public void setAutoColumnSpan(boolean autoColumnSpan) { this.autoColumnSpan = autoColumnSpan; } /** * @return <code>true</code> if automatic row spanning is enabled */ public boolean isAutoRowSpan() { return this.autoRowSpan; } /** * @param autoRowSpan * <code>true</code> to enable automatic row spanning, * <code>false</code> to disable it */ public void setAutoRowSpan(boolean autoRowSpan) { this.autoRowSpan = autoRowSpan; } @Override public void saveState(String prefix, Properties properties) { properties.setProperty( prefix + PERSISTENCE_KEY_AUTO_COLUMN_SPAN, Boolean.valueOf(this.autoColumnSpan).toString()); properties.setProperty( prefix + PERSISTENCE_KEY_AUTO_ROW_SPAN, Boolean.valueOf(this.autoRowSpan).toString()); if (this.autoSpanColumns.size() > 0) { StringBuilder strBuilder = new StringBuilder(); for (Integer index : this.autoSpanColumns) { strBuilder.append(index); strBuilder.append(IPersistable.VALUE_SEPARATOR); } properties.setProperty( prefix + PERSISTENCE_KEY_AUTO_SPAN_COLUMNS, strBuilder.toString()); } if (this.autoSpanRows.size() > 0) { StringBuilder strBuilder = new StringBuilder(); for (Integer index : this.autoSpanRows) { strBuilder.append(index); strBuilder.append(IPersistable.VALUE_SEPARATOR); } properties.setProperty( prefix + PERSISTENCE_KEY_AUTO_SPAN_ROWS, strBuilder.toString()); } } @Override public void loadState(String prefix, Properties properties) { String property = properties.getProperty(prefix + PERSISTENCE_KEY_AUTO_COLUMN_SPAN); if (property != null) { this.autoColumnSpan = Boolean.valueOf(property); } property = properties.getProperty(prefix + PERSISTENCE_KEY_AUTO_ROW_SPAN); if (property != null) { this.autoRowSpan = Boolean.valueOf(property); } this.autoSpanColumns.clear(); property = properties.getProperty(prefix + PERSISTENCE_KEY_AUTO_SPAN_COLUMNS); if (property != null) { List<Integer> newAutoSpanColumns = new ArrayList<Integer>(); StringTokenizer tok = new StringTokenizer(property, IPersistable.VALUE_SEPARATOR); while (tok.hasMoreTokens()) { String index = tok.nextToken(); newAutoSpanColumns.add(Integer.valueOf(index)); } this.autoSpanColumns.addAll(newAutoSpanColumns); } this.autoSpanRows.clear(); property = properties.getProperty(prefix + PERSISTENCE_KEY_AUTO_SPAN_ROWS); if (property != null) { List<Integer> newAutoSpanRows = new ArrayList<Integer>(); StringTokenizer tok = new StringTokenizer(property, IPersistable.VALUE_SEPARATOR); while (tok.hasMoreTokens()) { String index = tok.nextToken(); newAutoSpanRows.add(Integer.valueOf(index)); } this.autoSpanRows.addAll(newAutoSpanRows); } } }