org.eclipse.nebula.widgets.nattable.layer.SizeConfig.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.nebula.widgets.nattable.layer.SizeConfig.java

Source

/*******************************************************************************
 * Copyright (c) 2012, 2013 Original authors 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:
 *     Original authors and others - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.layer;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.TreeMap;

import org.apache.commons.lang.StringUtils;
import org.eclipse.nebula.widgets.nattable.persistence.IPersistable;

/**
 * This class stores the size configuration of rows/columns within the NatTable.
 * 
 * Mixed mode (fixed/percentage sizing):<br>
 * The mixed mode is only working if percentage sizing is enabled globally, and 
 * the fixed sized positions are marked separately.
 */
public class SizeConfig implements IPersistable {

    public static final String PERSISTENCE_KEY_DEFAULT_SIZE = ".defaultSize"; //$NON-NLS-1$
    public static final String PERSISTENCE_KEY_DEFAULT_SIZES = ".defaultSizes"; //$NON-NLS-1$
    public static final String PERSISTENCE_KEY_SIZES = ".sizes"; //$NON-NLS-1$
    public static final String PERSISTENCE_KEY_RESIZABLE_BY_DEFAULT = ".resizableByDefault"; //$NON-NLS-1$
    public static final String PERSISTENCE_KEY_RESIZABLE_INDEXES = ".resizableIndexes"; //$NON-NLS-1$
    public static final String PERSISTENCE_KEY_PERCENTAGE_SIZING = ".percentageSizing"; //$NON-NLS-1$
    public static final String PERSISTENCE_KEY_PERCENTAGE_SIZING_INDEXES = ".percentageSizingIndexes"; //$NON-NLS-1$

    /**
     * The global default size of this {@link SizeConfig}.
     */
    private int defaultSize;
    /**
     * Map that contains default sizes per column.
     */
    private final Map<Integer, Integer> defaultSizeMap = new TreeMap<Integer, Integer>();
    /**
     * Map that contains sizes per column.
     */
    private final Map<Integer, Integer> sizeMap = new TreeMap<Integer, Integer>();
    /**
     * Map that contains the resizable information per row/column.
     */
    private final Map<Integer, Boolean> resizablesMap = new TreeMap<Integer, Boolean>();
    /**
     * The global resizable information of this {@link SizeConfig}.
     */
    private boolean resizableByDefault = true;
    /**
     * Map that contains the percentage sizing information per row/column.
     */
    private final Map<Integer, Boolean> percentageSizingMap = new TreeMap<Integer, Boolean>();
    /**
     * Flag to tell whether the sizing is done for pixel or percentage values.
     */
    private boolean percentageSizing = false;
    /**
     * The available space needed for percentage calculation on resizing.
     */
    private int availableSpace = -1;
    /**
     * Map that contains the real pixel size. Will only be used on percentage sizing.
     * This map is not persisted as it will be calculated on resize.
     */
    private final Map<Integer, Integer> realSizeMap = new TreeMap<Integer, Integer>();

    /**
     * Create a new {@link SizeConfig} with the given default size.
     * @param defaultSize The default size to use.
     */
    public SizeConfig(int defaultSize) {
        this.defaultSize = defaultSize;
    }

    // Persistence

    @Override
    public void saveState(String prefix, Properties properties) {
        properties.put(prefix + PERSISTENCE_KEY_DEFAULT_SIZE, String.valueOf(defaultSize));
        saveMap(defaultSizeMap, prefix + PERSISTENCE_KEY_DEFAULT_SIZES, properties);
        saveMap(sizeMap, prefix + PERSISTENCE_KEY_SIZES, properties);
        properties.put(prefix + PERSISTENCE_KEY_RESIZABLE_BY_DEFAULT, String.valueOf(resizableByDefault));
        saveMap(resizablesMap, prefix + PERSISTENCE_KEY_RESIZABLE_INDEXES, properties);
        properties.put(prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING, String.valueOf(percentageSizing));
        saveMap(percentageSizingMap, prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING_INDEXES, properties);
    }

    private void saveMap(Map<Integer, ?> map, String key, Properties properties) {
        if (map.size() > 0) {
            StringBuilder strBuilder = new StringBuilder();
            for (Integer index : map.keySet()) {
                strBuilder.append(index);
                strBuilder.append(':');
                strBuilder.append(map.get(index));
                strBuilder.append(',');
            }
            properties.setProperty(key, strBuilder.toString());
        }
    }

    @Override
    public void loadState(String prefix, Properties properties) {
        //ensure to cleanup the current states prior loading new ones
        defaultSizeMap.clear();
        sizeMap.clear();
        resizablesMap.clear();

        String persistedDefaultSize = properties.getProperty(prefix + PERSISTENCE_KEY_DEFAULT_SIZE);
        if (!StringUtils.isEmpty(persistedDefaultSize)) {
            defaultSize = Integer.valueOf(persistedDefaultSize).intValue();
        }

        String persistedResizableDefault = properties.getProperty(prefix + PERSISTENCE_KEY_RESIZABLE_BY_DEFAULT);
        if (!StringUtils.isEmpty(persistedResizableDefault)) {
            resizableByDefault = Boolean.valueOf(persistedResizableDefault).booleanValue();
        }

        String persistedPercentageSizing = properties.getProperty(prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING);
        if (!StringUtils.isEmpty(persistedPercentageSizing)) {
            setPercentageSizing(Boolean.valueOf(persistedPercentageSizing).booleanValue());
        }

        loadBooleanMap(prefix + PERSISTENCE_KEY_RESIZABLE_INDEXES, properties, resizablesMap);
        loadIntegerMap(prefix + PERSISTENCE_KEY_DEFAULT_SIZES, properties, defaultSizeMap);
        loadIntegerMap(prefix + PERSISTENCE_KEY_SIZES, properties, sizeMap);
        loadBooleanMap(prefix + PERSISTENCE_KEY_PERCENTAGE_SIZING_INDEXES, properties, percentageSizingMap);
    }

    private void loadIntegerMap(String key, Properties properties, Map<Integer, Integer> map) {
        String property = properties.getProperty(key);
        if (property != null) {
            map.clear();

            StringTokenizer tok = new StringTokenizer(property, ","); //$NON-NLS-1$
            while (tok.hasMoreTokens()) {
                String token = tok.nextToken();
                int separatorIndex = token.indexOf(':');
                map.put(Integer.valueOf(token.substring(0, separatorIndex)),
                        Integer.valueOf(token.substring(separatorIndex + 1)));
            }
        }
    }

    private void loadBooleanMap(String key, Properties properties, Map<Integer, Boolean> map) {
        String property = properties.getProperty(key);
        if (property != null) {
            StringTokenizer tok = new StringTokenizer(property, ","); //$NON-NLS-1$
            while (tok.hasMoreTokens()) {
                String token = tok.nextToken();
                int separatorIndex = token.indexOf(':');
                map.put(Integer.valueOf(token.substring(0, separatorIndex)),
                        Boolean.valueOf(token.substring(separatorIndex + 1)));
            }
        }
    }

    // Default size

    public void setDefaultSize(int size) {
        if (size < 0) {
            throw new IllegalArgumentException("size < 0"); //$NON-NLS-1$
        }
        this.defaultSize = size;
    }

    public void setDefaultSize(int position, int size) {
        if (defaultSize < 0) {
            throw new IllegalArgumentException("size < 0"); //$NON-NLS-1$
        }
        defaultSizeMap.put(position, size);
    }

    private int getDefaultSize(int position) {
        Integer size = defaultSizeMap.get(position);
        if (size != null) {
            return size.intValue();
        } else {
            return defaultSize;
        }
    }

    // Size

    public int getAggregateSize(int position) {
        if (position < 0) {
            return -1;
        } else if (position == 0) {
            return 0;
        } else if (isAllPositionsSameSize() && !isPercentageSizing()) {
            //if percentage sizing is used, the sizes in defaultSize are used as percentage values
            //and not as pixel values, therefore another value needs to be considered
            return position * defaultSize;
        } else {
            int resizeAggregate = 0;
            int resizedColumns = 0;

            Map<Integer, Integer> mapToUse = isPercentageSizing() ? realSizeMap : sizeMap;

            for (Integer resizedPosition : mapToUse.keySet()) {
                if (resizedPosition.intValue() < position) {
                    resizedColumns++;
                    resizeAggregate += mapToUse.get(resizedPosition);
                } else {
                    break;
                }
            }

            return (position * defaultSize) + (resizeAggregate - (resizedColumns * defaultSize));
        }
    }

    public int getSize(int position) {
        Integer size;
        if (isPercentageSizing()) {
            size = realSizeMap.get(position);
        } else {
            size = sizeMap.get(position);
        }
        if (size != null) {
            return size.intValue();
        } else {
            return getDefaultSize(position);
        }
    }

    /**
     * Sets the given size for the given position. This method can be called manually for configuration
     * via {@link DataLayer} and will be called on resizing within the rendered UI. This is why there
     * is a check for percentage configuration. If this {@link SizeConfig} is configured to not use
     * percentage sizing, the size is taken as is. If percentage sizing is enabled, the given size
     * will be calculated to percentage value based on the already known pixel values.
     * <p>
     * If you want to use percentage sizing you should use {@link SizeConfig#setPercentage(int, int)}
     * for manual size configuration to avoid unnecessary calculations.
     * 
     * @param position The position for which the size should be set.
     * @param size The size in pixels to set for the given position.
     */
    public void setSize(int position, int size) {
        if (size < 0) {
            throw new IllegalArgumentException("size < 0"); //$NON-NLS-1$
        }
        if (isPositionResizable(position)) {
            //check whether the given value should be remembered as is or if it needs to be calculated
            if (!isPercentageSizing(position)) {
                sizeMap.put(position, size);
            } else {
                if (availableSpace > 0) {
                    Double percentage = ((double) size * 100) / availableSpace;
                    sizeMap.put(position, percentage.intValue());
                }
            }

            if (isPercentageSizing())
                calculatePercentages(availableSpace, realSizeMap.size());
        }
    }

    /**
     * Will set the given percentage size information for the given position and will set the
     * given position to be sized via percentage value.
     * @param position The positions whose percentage sizing information should be set.
     * @param percentage The percentage value to set, always dependent on the available space
     *          for percentage sizing, which can be less than the real available space in case
     *          there are also positions that are configured for fixed size.
     */
    public void setPercentage(int position, int percentage) {
        if (percentage < 0) {
            throw new IllegalArgumentException("percentage < 0"); //$NON-NLS-1$
        }
        if (isPositionResizable(position)) {
            percentageSizingMap.put(position, Boolean.TRUE);
            sizeMap.put(position, percentage);
            realSizeMap.put(position, calculatePercentageValue(percentage, availableSpace));
            calculatePercentages(availableSpace, realSizeMap.size());
        }
    }

    // Resizable

    /**
     * @return The global resizable information of this {@link SizeConfig}.
     */
    public boolean isResizableByDefault() {
        return resizableByDefault;
    }

    /**
     * Checks if there is a special resizable configuration for the given position. If not the
     * global resizable information is returned.
     * @param position The position of the row/column for which the resizable information is requested.
     * @return <code>true</code> if the given row/column position is resizable,
     *          <code>false</code> if not.
     */
    public boolean isPositionResizable(int position) {
        Boolean resizable = resizablesMap.get(position);
        if (resizable != null) {
            return resizable.booleanValue();
        }
        return resizableByDefault;
    }

    /**
     * Sets the resizable configuration for the given row/column position.
     * @param position The position of the row/column for which the resizable configuration should be set.
     * @param resizable <code>true</code> if the given row/column position should be resizable,
     *          <code>false</code> if not.
     */
    public void setPositionResizable(int position, boolean resizable) {
        resizablesMap.put(position, resizable);
    }

    /**
     * Sets the global resizable configuration.
     * Will reset all special resizable configurations.
     * @param resizableByDefault <code>true</code> if all rows/columns should be resizable,
     *          <code>false</code> if no row/column should be resizable.
     */
    public void setResizableByDefault(boolean resizableByDefault) {
        this.resizablesMap.clear();
        this.resizableByDefault = resizableByDefault;
    }

    // All positions same size

    public boolean isAllPositionsSameSize() {
        return defaultSizeMap.size() == 0 && sizeMap.size() == 0;
    }

    /**
     * @return <code>true</code> if the size of at least one position is interpreted in percentage,
     *          <code>false</code> if the size of all positions is interpreted by pixel.
     */
    public boolean isPercentageSizing() {
        if (!this.percentageSizingMap.isEmpty()) {
            for (Boolean pSize : this.percentageSizingMap.values()) {
                if (pSize)
                    return true;
            }
        }
        return this.percentageSizing;
    }

    /**
     * @param percentageSizing <code>true</code> if the size of the positions should be interpreted percentaged,
     *          <code>false</code> if the size of the positions should be interpreted by pixel.
     */
    public void setPercentageSizing(boolean percentageSizing) {
        this.percentageSizing = percentageSizing;
    }

    /**
     * Checks if there is a special percentage sizing configuration for the given position. If not the
     * global percentage sizing information is returned.
     * @param position The position of the row/column for which the percentage sizing information is requested.
     * @return <code>true</code> if the given row/column position is sized by percentage value,
     *          <code>false</code> if not.
     */
    public boolean isPercentageSizing(int position) {
        Boolean percentageSizing = percentageSizingMap.get(position);
        if (percentageSizing != null) {
            return percentageSizing;
        }
        return this.percentageSizing;
    }

    /**
     * Sets the percentage sizing configuration for the given row/column position.
     * @param position The position of the row/column for which the percentage sizing configuration should be set.
     * @param percentageSizing <code>true</code> if the given row/column position should be interpreted in percentage,
     *          <code>false</code> if not.
     */
    public void setPercentageSizing(int position, boolean percentageSizing) {
        percentageSizingMap.put(position, percentageSizing);
    }

    /**
     * Will calculate the real pixel values for the positions if percentage sizing is enabled.
     * @param space The space that is available for rendering.
     * @param positionCount The number of positions that should be handled by this {@link SizeConfig}
     */
    public void calculatePercentages(int space, int positionCount) {
        if (isPercentageSizing()) {
            this.availableSpace = space;

            int percentageSpace = calculateAvailableSpace(space);

            int sum = 0;
            int real = 0;
            int realSum = 0;
            int fixedSum = 0;
            List<Integer> noInfoPositions = new ArrayList<Integer>();
            Integer positionValue = null;
            for (int i = 0; i < positionCount; i++) {
                positionValue = this.sizeMap.get(i);
                if (positionValue != null) {
                    if (isPercentageSizing(i)) {
                        sum += positionValue;
                        real = calculatePercentageValue(positionValue, percentageSpace);
                    } else {
                        real = positionValue;
                        fixedSum += real;
                    }
                    realSum += real;
                    this.realSizeMap.put(i, real);
                } else {
                    //remember the position for which no size information exists
                    //needed to calculate the size for those positions dependent on the 
                    //remaining space
                    noInfoPositions.add(i);
                }
            }

            int[] correction = correctPercentageValues(sum, positionCount);
            if (correction != null) {
                sum = correction[0];
                realSum = correction[1] + fixedSum;
            }

            if (!noInfoPositions.isEmpty()) {
                //now calculate the size for the remaining columns
                double remaining = new Double(space) - realSum;
                Double remainingColSpace = remaining / noInfoPositions.size();
                for (Integer position : noInfoPositions) {
                    sum += (remainingColSpace / space) * 100;
                    this.realSizeMap.put(position, remainingColSpace.intValue());
                }
                //If there are positions for which no size information exist, the size config
                //will use 100 percent of the available space on percentage sizing. To handle
                //rounding issues just set the sum to 100 for correct calculation results.
                sum = 100;
            }
            if (sum == 100) {
                //check if the sum of the calculated values is the same as the given space
                //if not add the missing pixels to the last value
                //this is needed because of rounding issues on 100% with odd-numbered pixel values
                int valueSum = 0;
                int lastPos = -1;
                for (Map.Entry<Integer, Integer> entry : this.realSizeMap.entrySet()) {
                    valueSum += entry.getValue();
                    lastPos = Math.max(lastPos, entry.getKey());
                }

                if (valueSum < space) {
                    int lastPosValue = this.realSizeMap.get(lastPos);
                    this.realSizeMap.put(lastPos, lastPosValue + (space - valueSum));
                }
            }
        }
    }

    /**
     * @param percentage The percentage value.
     * @param space The available space
     * @return The percentage value of the given space.
     */
    private int calculatePercentageValue(int percentage, int space) {
        double factor = (double) percentage / 100;
        return new Double(space * factor).intValue();
    }

    /**
     * Calculates the available space for percentage size calculation.
     * This is necessary to support mixed mode of sizing, e.g. if two columns are configured
     * to have fixed size of 50 pixels and one column that should take the rest of the available
     * space of 500 pixels, the available space for percentage sizing is 400 pixels.
     * @param space The whole available space for rendering.
     * @return The available space for percentage sizing. Might be negative if the width of all
     *          fixed sized positions is greater than the available space.
     */
    private int calculateAvailableSpace(int space) {
        if (!this.percentageSizingMap.isEmpty()) {
            if (this.percentageSizing) {
                for (Map.Entry<Integer, Boolean> entry : this.percentageSizingMap.entrySet()) {
                    if (!entry.getValue()) {
                        if (this.sizeMap.containsKey(entry.getKey()))
                            space -= this.sizeMap.get(entry.getKey());
                    }
                }
            }
        }
        return space;
    }

    /**
     * This method is used to correct the calculated percentage values in case a user configured
     * more than 100 percent. In that case the set percentage values are scaled down to not exceed
     * 100 percent.
     * @param sum The sum of all configured percentage sized positions.
     * @param positionCount The number of positions to check.
     * @return Integer array with the sum value at first position and the new calculated real pixel 
     *          sum at second position in case a corrections took place. Will return <code>null</code>
     *          in case no correction happened.
     */
    private int[] correctPercentageValues(int sum, int positionCount) {
        Map<Integer, Integer> toModify = new TreeMap<Integer, Integer>();
        for (int i = 0; i < positionCount; i++) {
            Integer positionValue = this.sizeMap.get(i);
            if (positionValue != null && isPercentageSizing(i)) {
                toModify.put(i, this.realSizeMap.get(i));
            }
        }

        //if the sum is greater than 100 we need to normalize the percentage values
        if (sum > 100) {
            //calculate the factor which needs to be used to normalize the values
            double factor = Double.valueOf(100) / Double.valueOf(sum);

            //update the percentage size values by the calculated factor
            int realSum = 0;
            for (Map.Entry<Integer, Integer> mod : toModify.entrySet()) {
                int oldValue = mod.getValue();
                int newValue = Double.valueOf(oldValue * factor).intValue();
                realSum += newValue;
                this.realSizeMap.put(mod.getKey(), newValue);
            }

            return new int[] { 100, realSum };
        }

        //the given sum is not greater than 100 so we do not have to modify anything
        return null;
    }

}