/*
* Copyright (c) 2005 Matthew Hall 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:
* Matthew Hall - initial API and implementation
*/
package org.eclipse.nebula.paperclips.core;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.nebula.paperclips.core.internal.util.Util;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
/**
* A wrapper Print which splits its child print into multiple columns.
* <p>
* This class is horizontally greedy. Greedy prints take up all the available
* space on the page.
* <p>
* ColumnPrint attempts to use the minimum possible vertical space on the page
* if isCompressed() returns true (the default). This behavior can be disabled
* by calling setCompressed(false).
*
* @author Matthew Hall
*/
public class ColumnPrint implements Print {
final Print target;
final int columns;
final int spacing;
boolean compressed;
/**
* Constructs a ColumnPrint with the given target, number of columns, and
* column spacing (expressed in points). 72 points = 1".
*
* @param target
* the print which will be split into columns.
* @param columns
* the number of columns to display
* @param spacing
* the spacing between each column.
*/
public ColumnPrint(Print target, int columns, int spacing) {
this(target, columns, spacing, true);
}
/**
* Constructs a ColumnPrint with the given target, column count, column
* spacing, and compression.
*
* @param target
* the print to display in columns.
* @param columns
* the number of columns to display.
* @param spacing
* the spacing between each column, expressed in points. 72
* points = 1".
* @param compressed
* whether the columns on the final page are to be
*/
public ColumnPrint(Print target, int columns, int spacing,
boolean compressed) {
Util.notNull(target);
if (spacing < 0)
PaperClips
.error(SWT.ERROR_INVALID_ARGUMENT, "spacing must be >= 0"); //$NON-NLS-1$
if (columns < 2)
PaperClips
.error(SWT.ERROR_INVALID_ARGUMENT, "columns must be >= 2"); //$NON-NLS-1$
this.target = target;
this.spacing = spacing;
this.columns = columns;
this.compressed = compressed;
}
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + columns;
result = prime * result + (compressed ? 1231 : 1237);
result = prime * result + spacing;
result = prime * result + ((target == null) ? 0 : target.hashCode());
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ColumnPrint other = (ColumnPrint) obj;
if (columns != other.columns)
return false;
if (compressed != other.compressed)
return false;
if (spacing != other.spacing)
return false;
if (target == null) {
if (other.target != null)
return false;
} else if (!target.equals(other.target))
return false;
return true;
}
/**
* Returns the target print being split into columns.
*
* @return the target print being split into columns.
*/
public Print getTarget() {
return target;
}
/**
* Returns the number of columns per page.
*
* @return the number of columns per page.
*/
public int getColumnCount() {
return columns;
}
/**
* Returns the spacing between columns, in points. 72 points = 1".
*
* @return the spacing between columns, in points.
*/
public int getColumnSpacing() {
return spacing;
}
/**
* Returns whether the columns are compressed to the smallest possible
* height on the last page.
*
* @return whether the columns are compressed to the smallest possible
* height on the last page.
*/
public boolean isCompressed() {
return compressed;
}
/**
* Sets whether the columns are compressed to the smallest possible height
* on the last page.
*
* @param compressed
* whether to compress the columns.
*/
public void setCompressed(boolean compressed) {
this.compressed = compressed;
}
public PrintIterator iterator(Device device, GC gc) {
return new ColumnIterator(this, device, gc);
}
}
class ColumnIterator implements PrintIterator {
private PrintIterator target;
private final int columns;
private final int spacing;
private final boolean compressed;
ColumnIterator(ColumnPrint print, Device device, GC gc) {
this.target = print.target.iterator(device, gc);
this.columns = print.columns;
this.spacing = Math.round(print.spacing * device.getDPI().x / 72f);
this.compressed = print.compressed;
}
ColumnIterator(ColumnIterator that) {
this.target = that.target.copy();
this.columns = that.columns;
this.spacing = that.spacing;
this.compressed = that.compressed;
}
public Point minimumSize() {
return computeSize(target.minimumSize());
}
public Point preferredSize() {
return computeSize(target.preferredSize());
}
private Point computeSize(Point targetSize) {
return new Point(targetSize.x * columns + spacing * (columns - 1),
targetSize.y);
}
public boolean hasNext() {
return target.hasNext();
}
int[] computeColSizes(int width) {
int[] colSizes = new int[columns];
int availableWidth = width - spacing * (columns - 1);
for (int i = 0; i < colSizes.length; i++) {
colSizes[i] = availableWidth / (columns - i);
availableWidth -= colSizes[i];
}
return colSizes;
}
Point[] computeColOffsets(int[] colSizes) {
Point[] colOffsets = new Point[columns];
int xOffset = 0;
for (int i = 0; i < columns; i++) {
colOffsets[i] = new Point(xOffset, 0);
xOffset += colSizes[i] + spacing;
}
return colOffsets;
}
/**
* Iterates across the given column sizes and returns an array of
* PrintPieces to fill those columns, or null if there was insufficient room
* to continue iterating. A backup of the given iterator should be taken
* before invoking this method! If null is returned, the given iterator is
* corrupt and should no longer be used!
*
* @param colSizes
* an array of column sizes
* @param height
* the height
* @return an array of PrintPieces for the given column sizes, or null
*/
PrintPiece[] nextColumns(PrintIterator iterator, int[] colSizes, int height) {
List pieces = new ArrayList();
for (int i = 0; i < columns && iterator.hasNext(); i++) {
PrintPiece piece = PaperClips.next(iterator, colSizes[i], height);
if (piece == null)
return disposePieces(pieces);
pieces.add(piece);
}
return (PrintPiece[]) pieces.toArray(new PrintPiece[pieces.size()]);
}
private PrintPiece[] disposePieces(List pieces) {
for (Iterator iter = pieces.iterator(); iter.hasNext();) {
PrintPiece piece = (PrintPiece) iter.next();
piece.dispose();
}
return null;
}
PrintPiece createResult(PrintPiece[] pieces, int[] colSizes) {
CompositeEntry[] entries = new CompositeEntry[pieces.length];
Point[] offsets = computeColOffsets(colSizes);
for (int i = 0; i < pieces.length; i++)
entries[i] = new CompositeEntry(pieces[i], offsets[i]);
return new CompositePiece(entries);
}
public PrintPiece next(int width, int height) {
int[] colSizes = computeColSizes(width);
// Iterate on a copy in case any column fails to layout.
PrintIterator iter = target.copy();
PrintPiece[] columns = nextColumns(iter, colSizes, height);
if (columns == null)
return null;
// The target was completely consumed. If compressed property is true,
// close the gap until we find the
// smallest height that completely consumes the target's contents.
if (!iter.hasNext() && compressed)
return nextCompressed(colSizes, iter, columns);
this.target = iter;
return createResult(columns, colSizes);
}
private PrintPiece nextCompressed(int[] colSizes, PrintIterator iter,
PrintPiece[] columns) {
int highestInvalidHeight = 0;
int lowestValidHeight = getMaxHeight(columns);
// Remember the best results
PrintIterator bestIteration = iter;
PrintPiece[] bestColumns = columns;
while (lowestValidHeight > highestInvalidHeight + 1) {
int testHeight = (lowestValidHeight + highestInvalidHeight + 1) / 2;
iter = target.copy();
columns = nextColumns(iter, colSizes, testHeight);
if (columns == null) {
highestInvalidHeight = testHeight;
} else if (iter.hasNext()) {
highestInvalidHeight = testHeight;
disposePieces(columns);
} else {
disposePieces(bestColumns);
bestIteration = iter;
bestColumns = columns;
lowestValidHeight = getMaxHeight(bestColumns);
}
}
// Now that we've narrowed down the target's best iteration, we can
// update the state of this iterator and
// return the result.
this.target = bestIteration;
return createResult(bestColumns, colSizes);
}
private int getMaxHeight(PrintPiece[] pieces) {
int result = 0;
for (int i = 0; i < pieces.length; i++)
result = Math.max(result, pieces[i].getSize().y);
return result;
}
private void disposePieces(PrintPiece[] pieces) {
for (int i = 0; i < pieces.length; i++)
pieces[i].dispose();
}
public PrintIterator copy() {
return new ColumnIterator(this);
}
}