/*
* Copyright 2008 Google Inc.
*
* Licensed 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 com.google.gwt.gen2.table.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* <p>
* A helper class that distributes available width across a set of columns.
* </p>
*
* <h3>The following algorithm is used to distribute the available width:</h3>
* <ol>
* <li>Calculate the percent difference between the current and preferred width
* of each column</li>
* <li>Order the columns in descending order according to their percent
* differences from preferred width</li>
* <li>For each iteration, distribute width into the first n columns such that
* their percent difference now equals that of the n+1 column</li>
* <li>If a column hits its minimum or maximum size, remove it from the list of
* columns</li>
* <li>If there are no more columns that can accept more/less width (they are
* all at their boundaries), return the undistributed width.</li>
* </ol>
*/
class ColumnResizer {
/**
* The resolution of the width of columns. The true target width of a column
* is usually a decimal, but column widths can only be represented as integers
* (ie. pixels). The resolution determines the maximum number of pixels that
* the calculations can be off by.
*
* Increasing the resolution will increase the speed of the algorithm and
* reduce the accuracy of the calculations. The resolution must be at least 1
* or errors will occur (because we cannot get perfect "0" accuracy).
*/
private static final int RESOLUTION = 1;
/**
* A class that contains the current and desired width of a column.
*/
static class ColumnWidthInfo {
private int minWidth;
private int maxWidth;
private int preferredWidth;
private int curWidth;
/**
* The new column width.
*/
private int newWidth = 0;
/**
* The required width to achieve the next level.
*/
private int requiredWidth;
/**
* Construct a new {@link ColumnWidthInfo}.
*
* @param minWidth the minimum width of the column
* @param maxWidth the maximum width of the column
* @param preferredWidth the preferred width of the column
* @param curWidth the current width of the column
*/
public ColumnWidthInfo(int minWidth, int maxWidth, int preferredWidth,
int curWidth) {
this.minWidth = minWidth;
this.maxWidth = maxWidth;
this.preferredWidth = preferredWidth;
this.curWidth = curWidth;
}
public int getCurrentWidth() {
return curWidth;
}
public int getMaximumWidth() {
// For calculation purposes, ensure maxWidth >= minWidth
if (hasMaximumWidth()) {
return Math.max(maxWidth, minWidth);
}
return maxWidth;
}
public int getMinimumWidth() {
return minWidth;
}
public int getNewWidth() {
return newWidth;
}
public int getPreferredWidth() {
return preferredWidth;
}
public boolean hasMaximumWidth() {
return maxWidth >= 0;
}
public boolean hasMinimumWidth() {
return minWidth >= 0;
}
public void setCurrentWidth(int curWidth) {
this.curWidth = curWidth;
}
public void setMaximumWidth(int maxWidth) {
this.maxWidth = maxWidth;
}
public void setMinimumWidth(int minWidth) {
this.minWidth = minWidth;
}
public void setPreferredWidth(int preferredWidth) {
this.preferredWidth = preferredWidth;
}
/**
* Get the percentage difference between the current column width and the
* preferred column width. A negative value indicates that the current width
* is less than the preferred width, while a positive value indicates that
* the current width is above the preferred width.
*
* @return the percentage difference
*/
double getPercentageDifference() {
return (newWidth - preferredWidth) / (double) preferredWidth;
}
int getRequiredWidth() {
return requiredWidth;
}
void setNewWidth(int newWidth) {
this.newWidth = newWidth;
}
void setRequiredWidth(int requiredWidth) {
this.requiredWidth = requiredWidth;
}
}
/**
* Distribute some width across a list of columns, respecting the minimum and
* maximum widths of the columns. The return value is the remaining width that
* could not be distributed due to constraints. If the return value is 0, all
* of the width has been distributed.
*
* @param columns the list of column width info
* @param width the width to distribute
* @return the width that could not be distributed
*/
public int distributeWidth(List<ColumnWidthInfo> columns, int width) {
// The new width defaults to the current width, within min/max range
for (ColumnWidthInfo info : columns) {
int curWidth = info.getCurrentWidth();
if (info.hasMinimumWidth() && curWidth < info.getMinimumWidth()) {
curWidth = info.getMinimumWidth();
} else if (info.hasMaximumWidth() && curWidth > info.getMaximumWidth()) {
curWidth = info.getMaximumWidth();
}
width -= (curWidth - info.getCurrentWidth());
info.setNewWidth(curWidth);
}
// Do not modify widths if there is nothing to distribute
if (width == 0) {
return 0;
}
// Copy the list of columns
List<ColumnWidthInfo> orderedColumns = new ArrayList<ColumnWidthInfo>(
columns);
// Sort the list of columns
if (width > 0) {
// Enlarge columns
Comparator<ColumnWidthInfo> comparator = new Comparator<ColumnWidthInfo>() {
public int compare(ColumnWidthInfo o1, ColumnWidthInfo o2) {
double diff1 = o1.getPercentageDifference();
double diff2 = o2.getPercentageDifference();
if (diff1 < diff2) {
return -1;
} else if (diff1 == diff2) {
return 0;
} else {
return 1;
}
}
};
Collections.sort(orderedColumns, comparator);
} else if (width < 0) {
// Shrink columns
Comparator<ColumnWidthInfo> comparator = new Comparator<ColumnWidthInfo>() {
public int compare(ColumnWidthInfo o1, ColumnWidthInfo o2) {
double diff1 = o1.getPercentageDifference();
double diff2 = o2.getPercentageDifference();
if (diff1 > diff2) {
return -1;
} else if (diff1 == diff2) {
return 0;
} else {
return 1;
}
}
};
Collections.sort(orderedColumns, comparator);
}
// Distribute the width
return distributeWidthImpl(orderedColumns, width);
}
private int distributeWidthImpl(List<ColumnWidthInfo> columns, int width) {
// Iterate until width can not longer be distributed
boolean growing = (width > 0);
boolean fullySynced = false;
int syncedColumns = 1;
while (columns.size() > 0 && width != 0) {
// Calculate the target difference at the next level
double targetDiff = getTargetDiff(columns, syncedColumns, width);
// Calculate the total required width to achieve the target difference
int totalRequired = 0;
for (int curIndex = 0; curIndex < syncedColumns; curIndex++) {
// Calculate the new width at the target diff
ColumnWidthInfo curInfo = columns.get(curIndex);
int preferredWidth = curInfo.getPreferredWidth();
int newWidth = (int) (targetDiff * preferredWidth) + preferredWidth;
// Compare the boundaries
if (growing) {
newWidth = Math.max(newWidth, curInfo.getCurrentWidth());
if (curInfo.hasMaximumWidth()) {
newWidth = Math.min(newWidth, curInfo.getMaximumWidth());
}
} else {
newWidth = Math.min(newWidth, curInfo.getCurrentWidth());
if (curInfo.hasMinimumWidth()) {
newWidth = Math.max(newWidth, curInfo.getMinimumWidth());
}
}
// Calculate the width required to achieve the new width
curInfo.setRequiredWidth(newWidth - curInfo.getNewWidth());
totalRequired += curInfo.getRequiredWidth();
}
// Calculate the percent of the required width that is available
double percentAvailable = 1.0;
if (totalRequired != 0) {
percentAvailable = Math.min(1.0, width / (double) totalRequired);
}
for (int curIndex = 0; curIndex < syncedColumns; curIndex++) {
// Determine the true width to add to the column
ColumnWidthInfo curInfo = columns.get(curIndex);
int required = (int) (percentAvailable * curInfo.getRequiredWidth());
// Make sure we get out of the loop by distributing at least 1
if (fullySynced) {
if (growing) {
required = Math.max(RESOLUTION, required);
} else {
required = Math.min(-RESOLUTION, required);
}
}
// Don't distribute more than the available width
if (growing && required > width) {
required = width;
} else if (!growing && required < width) {
required = width;
}
// Set the new width of the column
curInfo.setNewWidth(curInfo.getNewWidth() + required);
width -= required;
// Remove the column if it has reached its maximum/minimum width
boolean maxedOut = false;
if (growing && curInfo.hasMaximumWidth()) {
maxedOut = (curInfo.getNewWidth() >= curInfo.getMaximumWidth());
} else if (!growing && curInfo.hasMinimumWidth()) {
maxedOut = (curInfo.getNewWidth() <= curInfo.getMinimumWidth());
}
if (maxedOut) {
columns.remove(curIndex);
curIndex--;
syncedColumns--;
}
}
// Increment the number of synced column
if (!fullySynced && syncedColumns < columns.size()) {
syncedColumns++;
} else {
fullySynced = true;
}
}
// Return the undistributed width
return width;
}
/**
* Calculate the target percentage difference of the next level.
*
* @param columns the column width info
* @param syncedColumns the number of synced columns
* @param width the width to distribute
*/
private double getTargetDiff(List<ColumnWidthInfo> columns,
int syncedColumns, int width) {
if (syncedColumns < columns.size()) {
// Use the diff of the next un-synced column as the target
return columns.get(syncedColumns).getPercentageDifference();
} else {
// Calculate the total diff after all width has been distributed
int totalNewWidth = width;
int totalPreferredWidth = 0;
for (ColumnWidthInfo info : columns) {
totalNewWidth += info.getNewWidth();
totalPreferredWidth += info.getPreferredWidth();
}
return (totalNewWidth - totalPreferredWidth)
/ (double) totalPreferredWidth;
}
}
}