001/*
002 * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003 *
004 * Redistribution and use in source and binary forms, with or without
005 * modification, are permitted provided that the following conditions are met:
006 *
007 *  o Redistributions of source code must retain the above copyright notice,
008 *    this list of conditions and the following disclaimer.
009 *
010 *  o Redistributions in binary form must reproduce the above copyright notice,
011 *    this list of conditions and the following disclaimer in the documentation
012 *    and/or other materials provided with the distribution.
013 *
014 *  o Neither the name of JGoodies Karsten Lentzsch nor the names of
015 *    its contributors may be used to endorse or promote products derived
016 *    from this software without specific prior written permission.
017 *
018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030
031package com.jgoodies.forms.layout;
032
033import java.awt.Container;
034import java.io.Serializable;
035import java.util.List;
036import java.util.Locale;
037import java.util.StringTokenizer;
038
039/**
040 * An abstract class that specifies columns and rows in FormLayout
041 * by their default alignment, start size and resizing behavior.
042 * API users will use the subclasses {@link ColumnSpec} and  {@link RowSpec}.
043 *
044 * @author        Karsten Lentzsch
045 * @version $Revision$
046 *
047 * @see     ColumnSpec
048 * @see     RowSpec
049 * @see     FormLayout
050 * @see     CellConstraints
051 */
052@SuppressWarnings("serial")
053public abstract class FormSpec implements Serializable {
054
055    // Horizontal and Vertical Default Alignments ***************************
056
057    /**
058     * By default put components in the left.
059     */
060    static final DefaultAlignment LEFT_ALIGN = new DefaultAlignment("left");
061
062    /**
063     * By default put components in the right.
064     */
065    static final DefaultAlignment RIGHT_ALIGN = new DefaultAlignment("right");
066
067    /**
068     * By default put the components in the top.
069     */
070    static final DefaultAlignment TOP_ALIGN = new DefaultAlignment("top");
071
072    /**
073     * By default put the components in the bottom.
074     */
075    static final DefaultAlignment BOTTOM_ALIGN = new DefaultAlignment("bottom");
076
077    /**
078     * By default put the components in the center.
079     */
080    static final DefaultAlignment CENTER_ALIGN = new DefaultAlignment("center");
081
082    /**
083     * By default fill the column or row.
084     */
085    static final DefaultAlignment FILL_ALIGN = new DefaultAlignment("fill");
086
087    /**
088     * An array of all enumeration values used to canonicalize
089     * deserialized default alignments.
090     */
091    private static final DefaultAlignment[] VALUES = { LEFT_ALIGN, RIGHT_ALIGN,
092            TOP_ALIGN, BOTTOM_ALIGN, CENTER_ALIGN, FILL_ALIGN };
093
094    // Resizing Weights *****************************************************
095
096    /**
097     * Gives a column or row a fixed size.
098     */
099    public static final double NO_GROW = 0.0d;
100
101    /**
102     * The default resize weight.
103     */
104    public static final double DEFAULT_GROW = 1.0d;
105
106    // Fields ***************************************************************
107
108    /**
109     * Holds the default alignment that will be used if a cell does not
110     * override this default.
111     */
112    private DefaultAlignment defaultAlignment;
113
114    /**
115     * Holds the size that describes how to size this column or row.
116     */
117    private Size size;
118
119    /**
120     * Holds the resize weight; is 0 if not used.
121     */
122    private double resizeWeight;
123
124    // Instance Creation ****************************************************
125
126    /**
127     * Constructs a <code>FormSpec</code> for the given default alignment,
128     * size, and resize weight. The resize weight must be a non-negative
129     * double; you can use <code>NONE</code> as a convenience value for no
130     * resize.
131     *
132     * @param defaultAlignment the spec's default alignment
133     * @param size             a constant, component or bounded size
134     * @param resizeWeight     the spec resize weight
135     * @exception IllegalArgumentException if the resize weight is negative
136     */
137    protected FormSpec(DefaultAlignment defaultAlignment, Size size,
138            double resizeWeight) {
139        this.defaultAlignment = defaultAlignment;
140        this.size = size;
141        this.resizeWeight = resizeWeight;
142        if (resizeWeight < 0) {
143            throw new IllegalArgumentException(
144                    "The resize weight must be non-negative.");
145        }
146    }
147
148    /**
149     * Constructs a <code>FormSpec</code> from the specified encoded
150     * description. The description will be parsed to set initial values.
151     *
152     * @param defaultAlignment         the default alignment
153     * @param encodedDescription        the encoded description
154     */
155    protected FormSpec(DefaultAlignment defaultAlignment,
156            String encodedDescription) {
157        this(defaultAlignment, Sizes.DEFAULT, NO_GROW);
158        parseAndInitValues(encodedDescription.toLowerCase(Locale.ENGLISH));
159    }
160
161    // Public API ***********************************************************
162
163    /**
164     * Returns the default alignment.
165     *
166     * @return the default alignment
167     */
168    public final DefaultAlignment getDefaultAlignment() {
169        return defaultAlignment;
170    }
171
172    /**
173     * Returns the size.
174     *
175     * @return the size
176     */
177    public final Size getSize() {
178        return size;
179    }
180
181    /**
182     * Returns the current resize weight.
183     *
184     * @return the resize weight.
185     */
186    public final double getResizeWeight() {
187        return resizeWeight;
188    }
189
190    /**
191     * Checks and answers whether this spec can grow or not.
192     * That is the case if and only if the resize weight is
193     * != <code>NO_GROW</code>.
194     *
195     * @return true if it can grow, false if it can't grow
196     */
197    final boolean canGrow() {
198        return getResizeWeight() != NO_GROW;
199    }
200
201    // Parsing **************************************************************
202
203    /**
204     * Parses an encoded form spec and initializes all required fields.
205     * The encoded description must be in lower case.
206     *
207     * @param encodedDescription   the FormSpec in an encoded format
208     * @exception IllegalArgumentException if the string is empty, has no size,
209     * or is otherwise invalid
210     */
211    private void parseAndInitValues(String encodedDescription) {
212        StringTokenizer tokenizer = new StringTokenizer(encodedDescription,
213                ":");
214        if (!tokenizer.hasMoreTokens()) {
215            throw new IllegalArgumentException(
216                    "The form spec must not be empty.");
217        }
218        String token = tokenizer.nextToken();
219
220        // Check if the first token is an orientation.
221        DefaultAlignment alignment = DefaultAlignment.valueOf(token,
222                isHorizontal());
223        if (alignment != null) {
224            defaultAlignment = alignment;
225            if (!tokenizer.hasMoreTokens()) {
226                throw new IllegalArgumentException(
227                        "The form spec must provide a size.");
228            }
229            token = tokenizer.nextToken();
230        }
231
232        parseAndInitSize(token);
233
234        if (tokenizer.hasMoreTokens()) {
235            resizeWeight = decodeResize(tokenizer.nextToken());
236        }
237    }
238
239    /**
240     * Parses an encoded size spec and initializes the size fields.
241     *
242     * @param token    a token that represents a size, either bounded or plain
243     */
244    private void parseAndInitSize(String token) {
245        if (token.startsWith("max(") && token.endsWith(")")) {
246            size = parseAndInitBoundedSize(token, false);
247            return;
248        }
249        if (token.startsWith("min(") && token.endsWith(")")) {
250            size = parseAndInitBoundedSize(token, true);
251            return;
252        }
253        size = decodeAtomicSize(token);
254    }
255
256    /**
257     * Parses an encoded compound size and sets the size fields.
258     * The compound size has format:
259     * max(&lt;atomic size&gt;;&lt;atomic size2&gt;) | min(&lt;atomic size1&gt;;&lt;atomic size2&gt;)
260     * One of the two atomic sizes must be a logical size, the other must
261     * be a size constant.
262     *
263     * @param token  a token for a bounded size, e.g. "max(50dlu; pref)"
264     * @param setMax  if true we set a maximum size, otherwise a minimum size
265     * @return a Size that represents the parse result
266     */
267    private Size parseAndInitBoundedSize(String token, boolean setMax) {
268        int semicolonIndex = token.indexOf(';');
269        String sizeToken1 = token.substring(4, semicolonIndex);
270        String sizeToken2 = token.substring(semicolonIndex + 1,
271                token.length() - 1);
272
273        Size size1 = decodeAtomicSize(sizeToken1);
274        Size size2 = decodeAtomicSize(sizeToken2);
275
276        // Check valid combinations and set min or max.
277        if (size1 instanceof ConstantSize) {
278            if (size2 instanceof Sizes.ComponentSize) {
279                return new BoundedSize(size2, setMax ? null : size1,
280                        setMax ? size1 : null);
281            }
282            throw new IllegalArgumentException(
283                    "Bounded sizes must not be both constants.");
284        }
285        if (size2 instanceof ConstantSize) {
286            return new BoundedSize(size1, setMax ? null : size2,
287                    setMax ? size2 : null);
288        }
289        throw new IllegalArgumentException(
290                "Bounded sizes must not be both logical.");
291    }
292
293    /**
294     * Decodes and returns an atomic size that is either a constant size or a
295     * component size.
296     *
297     * @param token        the encoded size
298     * @return the decoded size either a constant or component size
299     */
300    private Size decodeAtomicSize(String token) {
301        Sizes.ComponentSize componentSize = Sizes.ComponentSize.valueOf(token);
302        if (componentSize != null) {
303            return componentSize;
304        }
305        return ConstantSize.valueOf(token, isHorizontal());
306    }
307
308    /**
309     * Decodes an encoded resize mode and resize weight and answers
310     * the resize weight.
311     *
312     * @param token        the encoded resize weight
313     * @return the decoded resize weight
314     * @exception IllegalArgumentException if the string description is an
315     *     invalid string representation
316     */
317    private double decodeResize(String token) {
318        if (token.equals("g") || token.equals("grow")) {
319            return DEFAULT_GROW;
320        }
321        if (token.equals("n") || token.equals("nogrow")
322                || token.equals("none")) {
323            return NO_GROW;
324        }
325        // Must have format: grow(<double>)
326        if ((token.startsWith("grow(") || token.startsWith("g("))
327                && token.endsWith(")")) {
328            int leftParen = token.indexOf('(');
329            int rightParen = token.indexOf(')');
330            String substring = token.substring(leftParen + 1, rightParen);
331            return Double.parseDouble(substring);
332        }
333        throw new IllegalArgumentException("The resize argument '" + token
334                + "' is invalid. "
335                + " Must be one of: grow, g, none, n, grow(<double>), g(<double>)");
336    }
337
338    // Misc *****************************************************************
339
340    /**
341     * Returns a string representation of this form specification.
342     * The string representation consists of three elements separated by
343     * a colon (<tt>":"</tt>), first the alignment, second the size,
344     * and third the resize spec.<p>
345     *
346     * This method does <em>not</em> return a decoded version
347     * of this object; the contrary is the case. Many instances
348     * will return a string that cannot be parsed.<p>
349     *
350     * <strong>Note:</strong> The string representation may change
351     * at any time. It is strongly recommended to not use this string
352     * for parsing purposes.
353     *
354     * @return        a string representation of the form specification.
355     */
356    @Override
357    public final String toString() {
358        StringBuffer buffer = new StringBuffer();
359        buffer.append(defaultAlignment);
360
361        buffer.append(":");
362        buffer.append(size.toString());
363        buffer.append(':');
364        if (resizeWeight == NO_GROW) {
365            buffer.append("noGrow");
366        } else if (resizeWeight == DEFAULT_GROW) {
367            buffer.append("grow");
368        } else {
369            buffer.append("grow(");
370            buffer.append(resizeWeight);
371            buffer.append(')');
372        }
373        return buffer.toString();
374    }
375
376    /**
377     * Returns a string representation of this form specification.
378     * The string representation consists of three elements separated by
379     * a colon (<tt>":"</tt>), first the alignment, second the size,
380     * and third the resize spec.<p>
381     *
382     * This method does <em>not</em> return a decoded version
383     * of this object; the contrary is the case. Many instances
384     * will return a string that cannot be parsed.<p>
385     *
386     * <strong>Note:</strong> The string representation may change
387     * at any time. It is strongly recommended to not use this string
388     * for parsing purposes.
389     *
390     * @return  a string representation of the form specification.
391     */
392    public final String toShortString() {
393        StringBuffer buffer = new StringBuffer();
394        buffer.append(defaultAlignment.abbreviation());
395
396        buffer.append(":");
397        buffer.append(size.toString());
398        buffer.append(':');
399        if (resizeWeight == NO_GROW) {
400            buffer.append("n");
401        } else if (resizeWeight == DEFAULT_GROW) {
402            buffer.append("g");
403        } else {
404            buffer.append("g(");
405            buffer.append(resizeWeight);
406            buffer.append(')');
407        }
408        return buffer.toString();
409    }
410
411    // Abstract Behavior ****************************************************
412
413    /**
414     * Returns if this is a horizontal specification (vs. vertical).
415     * Used to distinct between horizontal and vertical dialog units,
416     * which have different conversion factors.
417     * @return true for horizontal, false for vertical
418     */
419    abstract boolean isHorizontal();
420
421    // Helper Code **********************************************************
422
423    /**
424     * Computes the maximum size for the given list of components, using
425     * this form spec and the specified measure.<p>
426     *
427     * Invoked by FormLayout to determine the size of one of my elements
428     *
429     * @param container       the layout container
430     * @param components      the list of components to measure
431     * @param minMeasure      the measure used to determine the minimum size
432     * @param prefMeasure     the measure used to determine the preferred size
433     * @param defaultMeasure  the measure used to determine the default size
434     * @return the maximum size in pixels
435     */
436    final int maximumSize(Container container, List components,
437            FormLayout.Measure minMeasure, FormLayout.Measure prefMeasure,
438            FormLayout.Measure defaultMeasure) {
439        return size.maximumSize(container, components, minMeasure, prefMeasure,
440                defaultMeasure);
441    }
442
443    /**
444     * An ordinal-based serializable typesafe enumeration for the
445     * column and row default alignment types.
446     */
447    public static final class DefaultAlignment implements Serializable {
448
449        private final transient String name;
450
451        private DefaultAlignment(String name) {
452            this.name = name;
453        }
454
455        /**
456         * Returns a DefaultAlignment that corresponds to the specified
457         * string, null if no such alignment exists.
458         *
459         * @param str        the encoded alignment
460         * @param isHorizontal   indicates the values orientation
461         * @return the corresponding DefaultAlignment or null
462         */
463        private static DefaultAlignment valueOf(String str,
464                boolean isHorizontal) {
465            if (str.equals("f") || str.equals("fill")) {
466                return FILL_ALIGN;
467            } else if (str.equals("c") || str.equals("center")) {
468                return CENTER_ALIGN;
469            } else if (isHorizontal) {
470                if (str.equals("r") || str.equals("right")) {
471                    return RIGHT_ALIGN;
472                } else if (str.equals("l") || str.equals("left")) {
473                    return LEFT_ALIGN;
474                } else {
475                    return null;
476                }
477            } else {
478                if (str.equals("t") || str.equals("top")) {
479                    return TOP_ALIGN;
480                } else if (str.equals("b") || str.equals("bottom")) {
481                    return BOTTOM_ALIGN;
482                } else {
483                    return null;
484                }
485            }
486        }
487
488        /**
489         * Returns this Alignment's name.
490         *
491         * @return this alignment's name.
492         */
493        @Override
494        public String toString() {
495            return name;
496        }
497
498        /**
499         * Returns the first character of this Alignment's name.
500         * Used to identify it in short format strings.
501         *
502         * @return the name's first character.
503         */
504        public char abbreviation() {
505            return name.charAt(0);
506        }
507
508        // Serialization *****************************************************
509
510        private static int nextOrdinal = 0;
511
512        private final int ordinal = nextOrdinal++;
513
514        private Object readResolve() {
515            return VALUES[ordinal]; // Canonicalize
516        }
517
518    }
519
520}