| /*=============================================================================# |
| # Copyright (c) 2013, 2021 Dirk Fauth and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 |
| # |
| # Contributors: |
| # Dirk Fauth <dirk.fauth@gmail.com> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ecommons.waltable.data; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Properties; |
| import java.util.StringTokenizer; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| |
| import org.eclipse.statet.ecommons.waltable.layer.cell.DataCell; |
| import org.eclipse.statet.ecommons.waltable.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 final List<Long> autoSpanColumns= new ArrayList<>(); |
| |
| /** |
| * 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 final List<Long> autoSpanRows= new ArrayList<>(); |
| |
| |
| /** |
| * |
| * @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(final IDataProvider underlyingDataProvider, |
| final boolean autoColumnSpan, final boolean autoRowSpan) { |
| this.underlyingDataProvider= underlyingDataProvider; |
| this.autoColumnSpan= autoColumnSpan; |
| this.autoRowSpan= autoRowSpan; |
| } |
| |
| @Override |
| public Object getDataValue(final long columnIndex, final long rowIndex, final int flags, final IProgressMonitor monitor) { |
| return this.underlyingDataProvider.getDataValue(columnIndex, rowIndex, flags, monitor); |
| } |
| |
| @Override |
| public void setDataValue(final long columnIndex, final long rowIndex, final Object newValue) { |
| this.underlyingDataProvider.setDataValue(columnIndex, rowIndex, newValue); |
| } |
| |
| @Override |
| public long getColumnCount() { |
| return this.underlyingDataProvider.getColumnCount(); |
| } |
| |
| @Override |
| public long getRowCount() { |
| return this.underlyingDataProvider.getRowCount(); |
| } |
| |
| @Override |
| public DataCell getCellByPosition(final long columnPosition, final long rowPosition) { |
| final long cellColumnPosition= isAutoSpanEnabledForColumn(columnPosition, rowPosition) ? |
| getStartColumnPosition(columnPosition, rowPosition) : columnPosition; |
| final long cellRowPosition= isAutoSpanEnabledForRow(columnPosition, rowPosition) ? |
| getStartRowPosition(columnPosition, rowPosition) : rowPosition; |
| |
| final long columnSpan= isAutoSpanEnabledForColumn(columnPosition, rowPosition) ? |
| getColumnSpan(cellColumnPosition, cellRowPosition) : 1; |
| final long 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(final long columnPosition, final long 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(final long columnPosition, final long 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(final long 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(final long 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(final Long... 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(final Long... 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(final Long... 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(final Long... 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 long getStartColumnPosition(final long columnPosition, final long rowPosition) { |
| if (columnPosition <= 0 || !isAutoSpanColumn(columnPosition) || !isAutoSpanColumn(columnPosition-1)) { |
| return columnPosition; |
| } |
| |
| //get value for the given column |
| final Object current= getDataValue(columnPosition, rowPosition, 0, null); |
| //get value of the column to the left |
| final Object before= getDataValue(columnPosition-1, rowPosition, 0, null); |
| |
| if (!Objects.equals(current, before)) { |
| //the both values are not equal, therefore return the given column position |
| return columnPosition; |
| } |
| |
| return getStartColumnPosition(columnPosition-1, rowPosition); |
| } |
| |
| /** |
| * 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 long getStartRowPosition(final long columnPosition, final long rowPosition) { |
| if (rowPosition <= 0 || !isAutoSpanRow(rowPosition) || !isAutoSpanRow(rowPosition - 1)) { |
| return rowPosition; |
| } |
| |
| //get value of given row |
| final Object current= getDataValue(columnPosition, rowPosition, 0, null); |
| //get value of row before |
| final Object before= getDataValue(columnPosition, rowPosition - 1, 0, null); |
| |
| if (!Objects.equals(current, before)) { |
| //the both values are not equal, therefore return the given row |
| return rowPosition; |
| } |
| |
| return getStartRowPosition(columnPosition, rowPosition-1); |
| } |
| |
| /** |
| * 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 long getColumnSpan(long columnPosition, final long rowPosition) { |
| long span= 1; |
| |
| while (columnPosition < getColumnCount()-1 |
| && isAutoSpanColumn(columnPosition) |
| && isAutoSpanColumn(columnPosition+1) |
| && Objects.equals( |
| getDataValue(columnPosition, rowPosition, 0, null), |
| getDataValue(columnPosition+1, rowPosition, 0, null) )) { |
| 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 long getRowSpan(final long columnPosition, long rowPosition) { |
| long span= 1; |
| |
| while (rowPosition < getRowCount() - 1 |
| && isAutoSpanRow(rowPosition) |
| && isAutoSpanRow(rowPosition + 1) |
| && Objects.equals( |
| getDataValue(columnPosition, rowPosition, 0, null), |
| getDataValue(columnPosition, rowPosition + 1, 0, null) )) { |
| span++; |
| rowPosition++; |
| } |
| return span; |
| } |
| |
| /** |
| * @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(final 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(final boolean autoRowSpan) { |
| this.autoRowSpan= autoRowSpan; |
| } |
| |
| @Override |
| public void saveState(final String prefix, final 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) { |
| final StringBuilder strBuilder= new StringBuilder(); |
| for (final Long 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) { |
| final StringBuilder strBuilder= new StringBuilder(); |
| for (final Long 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(final String prefix, final 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) { |
| final List<Long> newAutoSpanColumns= new ArrayList<>(); |
| final StringTokenizer tok= new StringTokenizer(property, IPersistable.VALUE_SEPARATOR); |
| while (tok.hasMoreTokens()) { |
| final String index= tok.nextToken(); |
| newAutoSpanColumns.add(Long.valueOf(index)); |
| } |
| |
| this.autoSpanColumns.addAll(newAutoSpanColumns); |
| } |
| |
| this.autoSpanRows.clear(); |
| property= properties.getProperty(prefix + PERSISTENCE_KEY_AUTO_SPAN_ROWS); |
| if (property != null) { |
| final List<Long> newAutoSpanRows= new ArrayList<>(); |
| final StringTokenizer tok= new StringTokenizer(property, IPersistable.VALUE_SEPARATOR); |
| while (tok.hasMoreTokens()) { |
| final String index= tok.nextToken(); |
| newAutoSpanRows.add(Long.valueOf(index)); |
| } |
| |
| this.autoSpanRows.addAll(newAutoSpanRows); |
| } |
| } |
| } |