com.privatejgoodies.forms.layout.FormSpec Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2013 JGoodies Software GmbH. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.privatejgoodies.forms.layout;
import static com.privatejgoodies.common.base.Preconditions.checkArgument;
import static com.privatejgoodies.common.base.Preconditions.checkNotBlank;
import static com.privatejgoodies.common.base.Preconditions.checkNotNull;
import java.awt.Container;
import java.io.Serializable;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* An abstract class that specifies columns and rows in FormLayout by their default alignment, start
* size and resizing behavior. API users will use the subclasses {@link ColumnSpec} and
* {@link RowSpec}
* .
*
* Also implements the parser for encoded column and row specifications and provides parser
* convenience behavior for its subclasses ColumnSpec and RowSpec.
*
* TODO: Consider extracting the parser role to a separate class.
*
* @author Karsten Lentzsch
* @version $Revision: 1.25 $
*
* @see ColumnSpec
* @see RowSpec
* @see FormLayout
* @see CellConstraints
*/
public abstract class FormSpec implements Serializable {
// Horizontal and Vertical Default Alignments ***************************
/**
* By default put components in the left.
*/
static final DefaultAlignment LEFT_ALIGN = new DefaultAlignment("left");
/**
* By default put components in the right.
*/
static final DefaultAlignment RIGHT_ALIGN = new DefaultAlignment("right");
/**
* By default put the components in the top.
*/
static final DefaultAlignment TOP_ALIGN = new DefaultAlignment("top");
/**
* By default put the components in the bottom.
*/
static final DefaultAlignment BOTTOM_ALIGN = new DefaultAlignment("bottom");
/**
* By default put the components in the center.
*/
static final DefaultAlignment CENTER_ALIGN = new DefaultAlignment("center");
/**
* By default fill the column or row.
*/
static final DefaultAlignment FILL_ALIGN = new DefaultAlignment("fill");
/**
* An array of all enumeration values used to canonicalize deserialized default alignments.
*/
private static final DefaultAlignment[] VALUES
= {LEFT_ALIGN, RIGHT_ALIGN, TOP_ALIGN, BOTTOM_ALIGN, CENTER_ALIGN, FILL_ALIGN};
// Resizing Weights *****************************************************
/**
* Gives a column or row a fixed size.
*/
public static final double NO_GROW = 0.0d;
/**
* The default resize weight.
*/
public static final double DEFAULT_GROW = 1.0d;
// Parser Patterns ******************************************************
private static final Pattern TOKEN_SEPARATOR_PATTERN
= Pattern.compile(":");
private static final Pattern BOUNDS_SEPARATOR_PATTERN
= Pattern.compile("\\s*,\\s*");
// Fields ***************************************************************
/**
* Holds the default alignment that will be used if a cell does not override this default.
*/
private DefaultAlignment defaultAlignment;
/**
* Holds the size that describes how to size this column or row.
*/
private Size size;
/**
* Holds the resize weight; is 0 if not used.
*/
private double resizeWeight;
// Instance Creation ****************************************************
/**
* Constructs a {@code FormSpec} for the given default alignment, size, and resize weight. The
* resize weight must be a non-negative double; you can use {@code NONE} as a convenience value
* for no resize.
*
* @param defaultAlignment the spec's default alignment
* @param size a constant, component or bounded size
* @param resizeWeight the spec resize weight
*
* @throws NullPointerException if the {@code size} is {@code null}
* @throws IllegalArgumentException if the {@code resizeWeight} is negative
*/
protected FormSpec(DefaultAlignment defaultAlignment,
Size size,
double resizeWeight) {
checkNotNull(size, "The size must not be null.");
checkArgument(resizeWeight >= 0, "The resize weight must be non-negative.");
this.defaultAlignment = defaultAlignment;
this.size = size;
this.resizeWeight = resizeWeight;
}
/**
* Constructs a FormSpec from the specified encoded description. The description will be parsed
* to set initial values.
*
* @param defaultAlignment the default alignment
* @param encodedDescription the encoded description
*/
protected FormSpec(DefaultAlignment defaultAlignment, String encodedDescription) {
this(defaultAlignment, Sizes.DEFAULT, NO_GROW);
parseAndInitValues(encodedDescription.toLowerCase(Locale.ENGLISH));
}
// Public API ***********************************************************
/**
* Returns the default alignment.
*
* @return the default alignment
*/
public final DefaultAlignment getDefaultAlignment() {
return defaultAlignment;
}
/**
* Returns the size.
*
* @return the size
*/
public final Size getSize() {
return size;
}
/**
* Returns the current resize weight.
*
* @return the resize weight.
*/
public final double getResizeWeight() {
return resizeWeight;
}
/**
* Checks and answers whether this spec can grow or not. That is the case if and only if the
* resize weight is != {@code NO_GROW}.
*
* @return true if it can grow, false if it can't grow
*/
final boolean canGrow() {
return getResizeWeight() != NO_GROW;
}
// Abstract Behavior ****************************************************
/**
* Returns if this is a horizontal specification (vs. vertical). Used to distinct between
* horizontal and vertical dialog units, which have different conversion factors.
*
* @return true for horizontal, false for vertical
*/
abstract boolean isHorizontal();
// Setting Values *********************************************************
void setDefaultAlignment(DefaultAlignment defaultAlignment) {
this.defaultAlignment = defaultAlignment;
}
void setSize(Size size) {
this.size = size;
}
void setResizeWeight(double resizeWeight) {
this.resizeWeight = resizeWeight;
}
// Parsing **************************************************************
/**
* Parses an encoded form specification and initializes all required fields. The encoded
* description must be in lower case.
*
* @param encodedDescription the FormSpec in an encoded format
*
* @throws NullPointerException if {@code encodedDescription} is {@code null}
* @throws IllegalArgumentException if {@code encodedDescription} is empty, whitespace, has no
* size, or is otherwise invalid
*/
private void parseAndInitValues(String encodedDescription) {
checkNotBlank(encodedDescription,
"The encoded form specification must not be null, empty or whitespace.");
String[] token = TOKEN_SEPARATOR_PATTERN.split(encodedDescription);
checkArgument(token.length > 0, "The form spec must not be empty.");
int nextIndex = 0;
String next = token[nextIndex++];
// Check if the first token is an orientation.
DefaultAlignment alignment = DefaultAlignment.valueOf(next, isHorizontal());
if (alignment != null) {
setDefaultAlignment(alignment);
checkArgument(token.length > 1, "The form spec must provide a size.");
next = token[nextIndex++];
}
setSize(parseSize(next));
if (nextIndex < token.length) {
setResizeWeight(parseResizeWeight(token[nextIndex]));
}
}
/**
* Parses an encoded size spec and returns the size.
*
* @param token a token that represents a size, either bounded or plain
* @return the decoded Size
*/
private Size parseSize(String token) {
if (token.startsWith("[") && token.endsWith("]")) {
return parseBoundedSize(token);
}
if (token.startsWith("max(") && token.endsWith(")")) {
return parseOldBoundedSize(token, false);
}
if (token.startsWith("min(") && token.endsWith(")")) {
return parseOldBoundedSize(token, true);
}
return parseAtomicSize(token);
}
private Size parseBoundedSize(String token) {
String content = token.substring(1, token.length() - 1);
String[] subtoken = BOUNDS_SEPARATOR_PATTERN.split(content);
Size basis = null;
Size lower = null;
Size upper = null;
if (subtoken.length == 2) {
Size size1 = parseAtomicSize(subtoken[0]);
Size size2 = parseAtomicSize(subtoken[1]);
if (isConstant(size1)) {
if (isConstant(size2)) {
lower = size1;
basis = size2;
upper = size2;
} else {
lower = size1;
basis = size2;
}
} else {
basis = size1;
upper = size2;
}
} else if (subtoken.length == 3) {
lower = parseAtomicSize(subtoken[0]);
basis = parseAtomicSize(subtoken[1]);
upper = parseAtomicSize(subtoken[2]);
}
if ((lower == null || isConstant(lower))
&& (upper == null || isConstant(upper))) {
return new BoundedSize(basis, lower, upper);
}
throw new IllegalArgumentException(
"Illegal bounded size '" + token + "'. Must be one of:"
+ "\n[,] // lower bound"
+ "\n[,] // upper bound"
+ "\n[,,] // lower and upper bound."
+ "\nExamples:"
+ "\n[50dlu,pref] // lower bound"
+ "\n[pref,200dlu] // upper bound"
+ "\n[50dlu,pref,200dlu] // lower and upper bound."
);
}
/**
* Parses an encoded compound size and sets the size fields. The compound size has format:
* max(<atomic size>;<atomic size2>) | min(<atomic size1>;<atomic
* size2>) One of the two atomic sizes must be a logical size, the other must be a size
* constant.
*
* @param token a token for a bounded size, e.g. "max(50dlu; pref)"
* @param setMax if true we set a maximum size, otherwise a minimum size
* @return a Size that represents the parse result
*/
private Size parseOldBoundedSize(String token, boolean setMax) {
int semicolonIndex = token.indexOf(';');
String sizeToken1 = token.substring(4, semicolonIndex);
String sizeToken2 = token.substring(semicolonIndex + 1, token.length() - 1);
Size size1 = parseAtomicSize(sizeToken1);
Size size2 = parseAtomicSize(sizeToken2);
// Check valid combinations and set min or max.
if (isConstant(size1)) {
if (size2 instanceof Sizes.ComponentSize) {
return new BoundedSize(size2, setMax ? null : size1,
setMax ? size1 : null);
}
throw new IllegalArgumentException(
"Bounded sizes must not be both constants.");
}
if (isConstant(size2)) {
return new BoundedSize(size1, setMax ? null : size2,
setMax ? size2 : null);
}
throw new IllegalArgumentException(
"Bounded sizes must not be both logical.");
}
/**
* Decodes and returns an atomic size that is either a constant size or a component size.
*
* @param token the encoded size
* @return the decoded size either a constant or component size
*/
private Size parseAtomicSize(String token) {
String trimmedToken = token.trim();
if (trimmedToken.startsWith("'") && trimmedToken.endsWith("'")) {
int length = trimmedToken.length();
if (length < 2) {
throw new IllegalArgumentException("Missing closing \"'\" for prototype.");
}
return new PrototypeSize(trimmedToken.substring(1, length - 1));
}
Sizes.ComponentSize componentSize = Sizes.ComponentSize.valueOf(trimmedToken);
if (componentSize != null) {
return componentSize;
}
return ConstantSize.valueOf(trimmedToken, isHorizontal());
}
/**
* Decodes an encoded resize mode and resize weight and answers the resize weight.
*
* @param token the encoded resize weight
* @return the decoded resize weight
* @throws IllegalArgumentException if the string description is an invalid string
* representation
*/
private static double parseResizeWeight(String token) {
if (token.equals("g") || token.equals("grow")) {
return DEFAULT_GROW;
}
if (token.equals("n") || token.equals("nogrow") || token.equals("none")) {
return NO_GROW;
}
// Must have format: grow()
if ((token.startsWith("grow(") || token.startsWith("g("))
&& token.endsWith(")")) {
int leftParen = token.indexOf('(');
int rightParen = token.indexOf(')');
String substring = token.substring(leftParen + 1, rightParen);
return Double.parseDouble(substring);
}
throw new IllegalArgumentException(
"The resize argument '" + token + "' is invalid. "
+ " Must be one of: grow, g, none, n, grow(), g()");
}
private static boolean isConstant(Size aSize) {
return aSize instanceof ConstantSize
|| aSize instanceof PrototypeSize;
}
// Misc *****************************************************************
/**
* Returns a string representation of this form specification. The string representation
* consists of three elements separated by a colon (":"), first the alignment, second
* the size, and third the resize spec.
*
* This method does not return an encoded version of this object; the contrary is the
* case. Many instances will return a string that cannot be parsed.
*
* Note: The string representation may change at any time. For parsing use
* {@link #encode()} instead.
*
* @return a string representation of the form specification.
*/
@Override
public final String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append(defaultAlignment);
buffer.append(":");
buffer.append(size.toString());
buffer.append(':');
if (resizeWeight == NO_GROW) {
buffer.append("noGrow");
} else if (resizeWeight == DEFAULT_GROW) {
buffer.append("grow");
} else {
buffer.append("grow(");
buffer.append(resizeWeight);
buffer.append(')');
}
return buffer.toString();
}
/**
* Returns a string representation of this form specification. The string representation
* consists of three elements separated by a colon (":"), first the alignment, second
* the size, and third the resize spec.
*
* This method does not return an encoded version of this object; the contrary is the
* case. Many instances will return a string that cannot be parsed.
*
* Note: The string representation may change at any time. For parsing use
* {@link #encode()} instead.
*
* @return a string representation of the form specification.
*/
public final String toShortString() {
StringBuffer buffer = new StringBuffer();
buffer.append(defaultAlignment.abbreviation());
buffer.append(":");
buffer.append(size.toString());
buffer.append(':');
if (resizeWeight == NO_GROW) {
buffer.append("n");
} else if (resizeWeight == DEFAULT_GROW) {
buffer.append("g");
} else {
buffer.append("g(");
buffer.append(resizeWeight);
buffer.append(')');
}
return buffer.toString();
}
/**
* Returns a short and parseable string representation of this form specification. The string
* will omit the alignment and resize specifications if these are the default values.
*
* @return a string representation of the form specification.
*
* @see #toShortString() for a more verbose string representation
*
* @since 1.2
*/
public final String encode() {
StringBuffer buffer = new StringBuffer();
DefaultAlignment alignmentDefault = isHorizontal()
? ColumnSpec.DEFAULT
: RowSpec.DEFAULT;
if (!alignmentDefault.equals(defaultAlignment)) {
buffer.append(defaultAlignment.abbreviation());
buffer.append(":");
}
buffer.append(size.encode());
if (resizeWeight == NO_GROW) {
// Omit the resize part
} else if (resizeWeight == DEFAULT_GROW) {
buffer.append(':');
buffer.append("g");
} else {
buffer.append(':');
buffer.append("g(");
buffer.append(resizeWeight);
buffer.append(')');
}
return buffer.toString();
}
// Helper Code **********************************************************
/**
* Computes the maximum size for the given list of components, using this form spec and the
* specified measure.
*
* Invoked by FormLayout to determine the size of one of my elements
*
* @param container the layout container
* @param components the list of components to measure
* @param minMeasure the measure used to determine the minimum size
* @param prefMeasure the measure used to determine the preferred size
* @param defaultMeasure the measure used to determine the default size
* @return the maximum size in pixels
*/
final int maximumSize(Container container,
List components,
FormLayout.Measure minMeasure,
FormLayout.Measure prefMeasure,
FormLayout.Measure defaultMeasure) {
return size.maximumSize(container,
components,
minMeasure,
prefMeasure,
defaultMeasure);
}
/**
* An ordinal-based serializable typesafe enumeration for the column and row default alignment
* types.
*/
public static final class DefaultAlignment implements Serializable {
private final transient String name;
private DefaultAlignment(String name) {
this.name = name;
}
/**
* Returns a DefaultAlignment that corresponds to the specified string, null if no such
* alignment exists.
*
* @param str the encoded alignment
* @param isHorizontal indicates the values orientation
* @return the corresponding DefaultAlignment or null
*/
private static DefaultAlignment valueOf(String str, boolean isHorizontal) {
if (str.equals("f") || str.equals("fill")) {
return FILL_ALIGN;
} else if (str.equals("c") || str.equals("center")) {
return CENTER_ALIGN;
} else if (isHorizontal) {
if (str.equals("r") || str.equals("right")) {
return RIGHT_ALIGN;
} else if (str.equals("l") || str.equals("left")) {
return LEFT_ALIGN;
} else {
return null;
}
} else if (str.equals("t") || str.equals("top")) {
return TOP_ALIGN;
} else if (str.equals("b") || str.equals("bottom")) {
return BOTTOM_ALIGN;
} else {
return null;
}
}
/**
* Returns this Alignment's name.
*
* @return this alignment's name.
*/
@Override
public String toString() {
return name;
}
/**
* Returns the first character of this Alignment's name. Used to identify it in short format
* strings.
*
* @return the name's first character.
*/
public char abbreviation() {
return name.charAt(0);
}
// Serialization *****************************************************
private static int nextOrdinal = 0;
private final int ordinal = nextOrdinal++;
private Object readResolve() {
return VALUES[ordinal]; // Canonicalize
}
}
}