1 /*
2 * Copyright 2006-2016 The JGUIraffe Team.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License")
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package net.sf.jguiraffe.gui.layout;
17
18 import java.io.Serializable;
19 import java.util.HashMap;
20 import java.util.Map;
21
22 import org.apache.commons.lang.StringUtils;
23 import org.apache.commons.lang.mutable.MutableObject;
24
25 /**
26 * <p>
27 * A class for describing column and row constraints for the
28 * {@link PercentLayout} layout manager.
29 * </p>
30 * <p>
31 * Objects of this class define properties for the single columns and rows that
32 * comprise the layout maintained by {@link PercentLayout}. Supported properties
33 * are:
34 * </p>
35 * <p>
36 * <ul>
37 * <li>The alignment of the represented cell. This can be one of the constants
38 * defined by the {@link CellAlignment} enumeration class.</li>
39 * <li>An initial size. This property defines how the column's or row's size
40 * depends on the sizes of the contained components. The possible values are
41 * defined by the enumeration class {@link CellSize}.</li>
42 * <li>A minimum size for this cell. This is a numeric value specifying the
43 * minimum width for columns and minimum height for rows. To determine the size
44 * of a column or row the layout first calculates the initial size. If this is
45 * less than the minimum size, the minimum size is used. It is possible to use
46 * different units for specifying this property.</li>
47 * <li>A weight factor. This factor determines how this cell should behave if
48 * additional space becomes available when the container's size changes (e.g. if
49 * the user resizes the window). If this factor is 0, the cell won't change its
50 * size; it will always keep its initial size. For other values than 0 the
51 * remaining space is given to the affected cells with regard to their factors.
52 * It is recommended to use percentage values for these factors, which sum up to
53 * 100 percent (hence the name of the {@code PercentLayout} layout manager).
54 * Then it is easy to understand that each cell with a weight factor greater
55 * than 0 is granted as much percent of the remaining space.</li>
56 * </ul>
57 * </p>
58 * <p>
59 * Instances of this class are not created directly, but the inner {@code
60 * Builder} class is used for this purpose (an application of the
61 * <em>builder</em> pattern). A typical invocation sequence could look as
62 * follows:
63 *
64 * <pre>
65 * CellConstraints.Builder ccb = new CellConstraints.Builder();
66 * CellConstraints cc = ccb.withCellSize(CellSize.Preferred).withMinimumSize(
67 * new NumberWithUnit(20, Unit.PIXEL)).withWeight(25).withCellAlignment(
68 * CellAlignment.FULL).create();
69 * </pre>
70 *
71 * </p>
72 * <p>
73 * For instances of this class a string representation is defined. Strings
74 * conforming to the format explained below can also be passed to the builder
75 * for creating instances:
76 * </p>
77 * <p>
78 *
79 * <pre>
80 * CONSTRAINT ::= [ALIGN"/"]SIZE["/"WEIGHT]
81 * SIZE ::= INITSIZE | MINSIZE | BOTHSIZES
82 * BOTHSIZES ::= INITSIZE "(" MINSIZE ")"
83 * INITSIZE ::= "PREFERRED" | "MINIMUM" | "NONE"
84 * MINSIZE ::= <Positive number> [UNIT]
85 * UNIT ::= "px" | "cm" | "in" | "dlu"
86 * ALIGN ::= "START" | "CENTER" | "END" | "FULL"
87 * WEIGHT ::= <Positive number>
88 * </pre>
89 *
90 * Here are some examples:
91 * <ul>
92 * <li>{@code PREFERRED}: only the cell size is set, the other properties are
93 * set to default values.</li>
94 * <li>{@code CENTER/MINIMUM(20px)/33}: A {@code CellConstraints} object is
95 * created with the {@link CellSize} {@code MINIMUM} and the alignment {@code
96 * CENTER}. The minimum size is set to 20 pixels, and the weight factor is set
97 * to 33.</li>
98 * <li>{@code (20)}: Here only the minimum size of the cell is set. The
99 * {@link CellSize} is automatically set to {@code NONE}.</li>
100 * </ul>
101 * </p>
102 * <p>
103 * Instances of this class are immutable and thus can be shared between multiple
104 * threads. The {@code Builder} class makes use of this feature and caches the
105 * instances that have been created. So if another constraint with already used
106 * properties is requested, a shared instance can be returned.
107 * </p>
108 *
109 * @author Oliver Heger
110 * @version $Id: CellConstraints.java 205 2012-01-29 18:29:57Z oheger $
111 */
112 public final class CellConstraints implements Serializable
113 {
114 /**
115 * The serial version UID.
116 */
117 private static final long serialVersionUID = 20090730L;
118
119 /** Constant for the minimum size start delimiter. */
120 private static final char MINSIZE_START = '(';
121
122 /** Constant for the minimum size end delimiter. */
123 private static final char MINSIZE_END = ')';
124
125 /** Constant for the separator used for cell constraints in string form. */
126 private static final String DELIMITER = "/";
127
128 /** Constant for the default string buffer size. */
129 private static final int BUF_SIZE = 32;
130
131 /** Constant for the maximum length of a parsed constraints specification. */
132 private static final int LEN_MAX = 3;
133
134 /** Constant for the middle length of a parsed constraints specification. */
135 private static final int LEN_MEDIUM = 2;
136
137 /** Constant for the minimum length of a parsed constraints specification. */
138 private static final int LEN_MIN = 1;
139
140 /** Stores the alignment. */
141 private final CellAlignment alignment;
142
143 /** Stores the cell size. */
144 private final CellSize cellSize;
145
146 /** Stores the cell's minimum size. */
147 private final NumberWithUnit minSize;
148
149 /** Stores the weight factor. */
150 private final int weight;
151
152 /**
153 * Creates a new instance of {@code CellConstraints} and initializes it.
154 * Client code uses the inner {@code Builder} class for creating instances.
155 *
156 * @param al the alignment
157 * @param sz the size
158 * @param minsz the minimum size
159 * @param w the weight factor
160 */
161 private CellConstraints(CellAlignment al, CellSize sz,
162 NumberWithUnit minsz, int w)
163 {
164 alignment = al;
165 minSize = NumberWithUnit.nonNull(minsz);
166 weight = w;
167 cellSize = sz;
168 }
169
170 /**
171 * Returns the alignment string.
172 *
173 * @return the alignment string
174 */
175 public CellAlignment getAlignment()
176 {
177 return alignment;
178 }
179
180 /**
181 * Returns the size of the cell.
182 *
183 * @return the cell size
184 */
185 public CellSize getCellSize()
186 {
187 return cellSize;
188 }
189
190 /**
191 * Returns the minimum size.
192 *
193 * @return the minimum size
194 */
195 public NumberWithUnit getMinSize()
196 {
197 return minSize;
198 }
199
200 /**
201 * Returns the weight factor.
202 *
203 * @return the weight factor
204 */
205 public int getWeight()
206 {
207 return weight;
208 }
209
210 /**
211 * Helper method for creating a specification string for the specified
212 * constraint data.
213 *
214 * @param align the alignment string
215 * @param size the cell size
216 * @param minsz the minimum size
217 * @param w the weight factor
218 * @return a string specification for the passed in data
219 */
220 private static String toString(CellAlignment align, CellSize size,
221 NumberWithUnit minsz, int w)
222 {
223 StringBuilder buf = new StringBuilder(BUF_SIZE);
224 buf.append(align.name()).append(DELIMITER);
225 buf.append(size.name());
226 buf.append(MINSIZE_START);
227 minsz.buildUnitString(buf);
228 buf.append(MINSIZE_END);
229 buf.append(DELIMITER).append(w);
230 return buf.toString();
231 }
232
233 /**
234 * Returns a string specification for this {@code CellConstraints} object.
235 * This string conforms to the definition given in the class comment. It can
236 * be passed to a {@code Builder} instance to obtain a corresponding {@code
237 * CellConstraints} instance. The string returned by this method contains
238 * the values of all properties of this object, even if some have been set
239 * to default values.
240 *
241 * @return a specification string for this instance
242 */
243 public String toSpecificationString()
244 {
245 return toString(getAlignment(), getCellSize(), getMinSize(),
246 getWeight());
247 }
248
249 /**
250 * Returns a string representation of this constraints object. This string
251 * contains the specification string produced by
252 * {@link #toSpecificationString()}.
253 *
254 * @return a string representation for this object
255 */
256 @Override
257 public String toString()
258 {
259 StringBuilder buf = new StringBuilder();
260 buf.append("CellConstraints [ ");
261 buf.append(toSpecificationString());
262 buf.append(" ]");
263 return buf.toString();
264 }
265
266 /**
267 * Compares this object with another one. Two instances of {@code
268 * CellConstraints} are equal if all of their properties are equal.
269 *
270 * @param obj the object to compare to
271 * @return a flag whether these objects are equal
272 */
273 @Override
274 public boolean equals(Object obj)
275 {
276 if (this == obj)
277 {
278 return true;
279 }
280 if (!(obj instanceof CellConstraints))
281 {
282 return false;
283 }
284
285 CellConstraints c = (CellConstraints) obj;
286 return getAlignment() == c.getAlignment()
287 && getCellSize() == c.getCellSize()
288 && getMinSize().equals(c.getMinSize())
289 && getWeight() == c.getWeight();
290 }
291
292 /**
293 * Returns a hash code for this object.
294 *
295 * @return a hash code for this object
296 */
297 @Override
298 public int hashCode()
299 {
300 final int factor = 31;
301 final int seed = 17;
302
303 int result = seed;
304 result = factor * result + getAlignment().hashCode();
305 result = factor * result + getCellSize().hashCode();
306 result = factor * result + getMinSize().hashCode();
307 result = factor * result + getWeight();
308
309 return result;
310 }
311
312 /**
313 * Helper method for parsing the minimum size. This method tests whether the
314 * passed in string contains a valid size definition. If this is the case,
315 * the corresponding values are returned in the values of the passed in
316 * mutable objects.
317 *
318 * @param sizeDef the string to be parsed
319 * @param size takes the value of the size
320 * @param minSize takes the value of the minimum size
321 * @return a flag if the size could be successfully parsed
322 */
323 private static boolean parseSize(String sizeDef, MutableObject size,
324 MutableObject minSize)
325 {
326 String s = sizeDef.trim();
327 int pos = s.indexOf(MINSIZE_START);
328 if (pos > 0)
329 {
330 // both initial size and minimum size
331 if (!s.endsWith(String.valueOf(MINSIZE_END)))
332 {
333 return false;
334 }
335
336 try
337 {
338 size.setValue(valueOf(CellSize.class, s.substring(0, pos)));
339 minSize.setValue(new NumberWithUnit(s.substring(pos + 1, s
340 .length() - 1)));
341 }
342 catch (IllegalArgumentException iex)
343 {
344 // not a valid literal or number with unit
345 return false;
346 }
347 }
348
349 else
350 {
351 try
352 {
353 // initial size defined?
354 size.setValue(valueOf(CellSize.class, s));
355 minSize.setValue(NumberWithUnit.ZERO);
356 }
357 catch (IllegalArgumentException iex)
358 {
359 // no, a minimum size?
360 try
361 {
362 minSize.setValue(new NumberWithUnit(s));
363 size.setValue(CellSize.NONE);
364 }
365 catch (IllegalArgumentException iex2)
366 {
367 return false;
368 }
369 }
370 }
371
372 return true;
373 }
374
375 /**
376 * Helper method for parsing and validating a size definition. This method
377 * works like {@link #parseSize(String, MutableObject, MutableObject)}, but
378 * throws an exception if parsing fails.
379 *
380 * @param s the string to be parsed
381 * @param size takes the value of the size
382 * @param minSize takes the value of the minimum size
383 */
384 private static void parseSizeEx(String s, MutableObject size,
385 MutableObject minSize)
386 {
387 if (!parseSize(s, size, minSize))
388 {
389 throw new IllegalArgumentException("Invalid size declaration: " + s);
390 }
391 }
392
393 /**
394 * Parses the given alignment specification.
395 *
396 * @param comps the components of the complete specification
397 * @param index the index of the component to be parsed
398 * @param defAlign the default alignment
399 * @return the alignment
400 * @throws IllegalArgumentException if the specification is invalid
401 */
402 private static CellAlignment parseAlignment(String[] comps, int index,
403 CellAlignment defAlign)
404 {
405 if (index < 0)
406 {
407 return defAlign;
408 }
409 else
410 {
411 return valueOf(CellAlignment.class, comps[index]);
412 }
413 }
414
415 /**
416 * Parses the given specification of a weight factor.
417 *
418 * @param comps the components of the complete specification
419 * @param index the index of the component to be parsed
420 * @return the weight factor
421 * @throws IllegalArgumentException if the specification is invalid
422 */
423 private static int parseWeight(String[] comps, int index)
424 {
425 if (index < 0)
426 {
427 return 0;
428 }
429
430 try
431 {
432 int weight = Integer.parseInt(comps[index].trim());
433 if (weight < 0)
434 {
435 throw new IllegalArgumentException(
436 "Weight factor must be positive: " + comps[index]);
437 }
438 return weight;
439 }
440 catch (NumberFormatException nfex)
441 {
442 throw new IllegalArgumentException(
443 "Invalid weight factor specification: " + comps[index]);
444 }
445 }
446
447 /**
448 * Helper method for obtaining an enumeration literal for a given case. The
449 * string is transformed to upper case before the literal is checked, so
450 * this method is case insensitive.
451 *
452 * @param <T> the type of the enum class
453 * @param enumType the type of the enumeration
454 * @param s the string
455 * @return the corresponding enum value
456 * @throws IllegalArgumentException if the string is not a valid enumeration
457 * literal
458 */
459 private static <T extends Enum<T>> T valueOf(Class<T> enumType, String s)
460 {
461 return Enum.valueOf(enumType, s.trim().toUpperCase());
462 }
463
464 /**
465 * Parses the given specification string for a {@code CellConstraints}
466 * object and creates a corresponding instance.
467 *
468 * @param spec the string to be parsed
469 * @param defAlign the default alignment
470 * @return the corresponding instance
471 */
472 private static CellConstraints parse(String spec, CellAlignment defAlign)
473 {
474 if (StringUtils.isEmpty(spec))
475 {
476 throw new IllegalArgumentException(
477 "Undefined specification string!");
478 }
479
480 MutableObject size = new MutableObject();
481 MutableObject minSize = new MutableObject();
482 int indexAlign = -1;
483 int indexWeight = -1;
484 String[] comps = spec.split(DELIMITER);
485
486 switch (comps.length)
487 {
488 case LEN_MAX:
489 indexAlign = 0;
490 indexWeight = 2;
491 parseSizeEx(comps[1], size, minSize);
492 break;
493
494 case LEN_MEDIUM:
495 if (parseSize(comps[0], size, minSize))
496 {
497 indexWeight = 1;
498 }
499 else
500 {
501 parseSizeEx(comps[1], size, minSize);
502 indexAlign = 0;
503 }
504 break;
505
506 case LEN_MIN:
507 parseSizeEx(comps[0], size, minSize);
508 break;
509
510 default:
511 throw new IllegalArgumentException(
512 "Invalid number of components in specification string: "
513 + spec);
514 }
515
516 NumberWithUnit nMinSize = (NumberWithUnit) minSize.getValue();
517 if (nMinSize.getValue() < 0)
518 {
519 throw new IllegalArgumentException(
520 "Minimum size must be non-negative: " + nMinSize);
521 }
522
523 return new CellConstraints(parseAlignment(comps, indexAlign, defAlign),
524 (CellSize) size.getValue(), nMinSize, parseWeight(comps,
525 indexWeight));
526 }
527
528 /**
529 * <p>
530 * A <em>builder</em> class for creating instances of {@code
531 * CellConstraints}.
532 * </p>
533 * <p>
534 * With the different {@code withXXXX()} methods the properties of the new
535 * {@code CellConstraints} object are defined. Then, with the {@code
536 * create()} method, the instance is actually created. Alternatively, with
537 * the {@code parse()} method a {@code CellConstraints} instance for a
538 * string representation can be requested.
539 * </p>
540 * <p>
541 * This class is not thread-safe. It maintains an internal (unsynchronized)
542 * cache of the {@code CellConstraints} instances that have already been
543 * created. If instances with equal properties are requested, the same
544 * instance is returned.
545 * </p>
546 * <p>
547 * More information including a usage example of this class can be found in
548 * the documentation for {@link CellConstraints}.
549 * </p>
550 *
551 * @author Oliver Heger
552 * @version $Id: CellConstraints.java 205 2012-01-29 18:29:57Z oheger $
553 */
554 public static final class Builder
555 {
556 /** The cache of instances created so far. */
557 private final Map<String, CellConstraints> cache;
558
559 /** The current cell size. */
560 private CellSize cellSize;
561
562 /** The current alignment. */
563 private CellAlignment alignment;
564
565 /** The default alignment. */
566 private CellAlignment defaultAlignment;
567
568 /** The current minimum size. */
569 private NumberWithUnit minSize;
570
571 /** The current weight factor. */
572 private int weight;
573
574 /**
575 * Creates a new instance of {@code Builder}.
576 */
577 public Builder()
578 {
579 cache = new HashMap<String, CellConstraints>();
580 defaultAlignment = CellAlignment.FULL;
581 reset();
582 }
583
584 /**
585 * Returns the default {@code CellAlignment} used by this builder.
586 *
587 * @return the default {@code CellAlignment}
588 */
589 public CellAlignment getDefaultAlignment()
590 {
591 return defaultAlignment;
592 }
593
594 /**
595 * Sets the default {@code CellAlignment} used by this builder. This
596 * alignment is set by the {@code reset()} method. Note: after calling
597 * this method {@code reset()} must be called to apply the new default
598 * alignment.
599 *
600 * @param defaultAlignment the new default alignment (must not be
601 * <b>null</b>)
602 * @throws IllegalArgumentException if the parameter is <b>null</b>
603 */
604 public void setDefaultAlignment(CellAlignment defaultAlignment)
605 {
606 if (defaultAlignment == null)
607 {
608 throw new IllegalArgumentException(
609 "Default alignment must not be null!");
610 }
611
612 this.defaultAlignment = defaultAlignment;
613 }
614
615 /**
616 * Sets the {@code size} property of the {@link CellConstraints}
617 * instance to be created.
618 *
619 * @param sz the size of the cell (must not be <b>null</b>)
620 * @return a reference to this builder for method chaining
621 * @throws IllegalArgumentException if the {@code CellSize} object is
622 * <b>null</b>
623 */
624 public Builder withCellSize(CellSize sz)
625 {
626 if (sz == null)
627 {
628 throw new IllegalArgumentException(
629 "Cell size must not be null!");
630 }
631 cellSize = sz;
632 return this;
633 }
634
635 /**
636 * Sets the {@code minimumSize} property of the {@link CellConstraints}
637 * instance to be created.
638 *
639 * @param minSize the minimum size of the cell (can be <b>null</b>, then
640 * a minimum size of 0 pixels is assumed)
641 * @return a reference to this builder for method chaining
642 * @throws IllegalArgumentException if the value of the minimum size is
643 * negative
644 */
645 public Builder withMinimumSize(NumberWithUnit minSize)
646 {
647 this.minSize = NumberWithUnit.nonNull(minSize);
648 if (this.minSize.getValue() < 0)
649 {
650 throw new IllegalArgumentException(
651 "Minimum size must not be negative: " + this.minSize);
652 }
653 return this;
654 }
655
656 /**
657 * Sets the {@code alignment} property of the {@link CellConstraints}
658 * instance to be created.
659 *
660 * @param align the alignment of the cell (must not be <b>null</b>)
661 * @return a reference to this builder for method chaining
662 * @throws IllegalArgumentException if the alignment is <b>null</b>
663 */
664 public Builder withCellAlignment(CellAlignment align)
665 {
666 if (align == null)
667 {
668 throw new IllegalArgumentException(
669 "Cell alignment must not be null!");
670 }
671 alignment = align;
672 return this;
673 }
674
675 /**
676 * Sets the {@code weight} property of the {@link CellConstraints}
677 * instance to be created. This must be a positive number.
678 *
679 * @param factor the weight factor
680 * @return a reference to this builder for method chaining
681 * @throws IllegalArgumentException if the number is less than 0
682 */
683 public Builder withWeight(int factor)
684 {
685 if (factor < 0)
686 {
687 throw new IllegalArgumentException(
688 "Weight factor must not be negative: " + factor);
689 }
690 weight = factor;
691 return this;
692 }
693
694 /**
695 * Initializes the properties of the {@link CellConstraints} instance to
696 * be created with default values for a column. This means:
697 * <ul>
698 * <li>the alignment is set to {@code FULL}</li>
699 * <li>the size is set to {@code PREFERRED}</li>
700 * <li>no minimum size is set</li>
701 * <li>the weight factor is set to 0</li>
702 * </ul>
703 * This method can be called first for initializing default values.
704 * Then, with other {@code withXXXX()} methods specific values can be
705 * set.
706 *
707 * @return a reference to this builder for method chaining
708 */
709 public Builder defaultColumn()
710 {
711 alignment = CellAlignment.FULL;
712 cellSize = CellSize.PREFERRED;
713 minSize = NumberWithUnit.ZERO;
714 weight = 0;
715 return this;
716 }
717
718 /**
719 * Initializes the properties of the {@link CellConstraints} instance to
720 * be created with default values for a row. This means:
721 * <ul>
722 * <li>the alignment is set to {@code CENTER}</li>
723 * <li>the size is set to {@code PREFERRED}</li>
724 * <li>no minimum size is set</li>
725 * <li>the weight factor is set to 0</li>
726 * </ul>
727 * This method can be called first for initializing default values.
728 * Then, with other {@code withXXXX()} methods specific values can be
729 * set.
730 *
731 * @return a reference to this builder for method chaining
732 */
733 public Builder defaultRow()
734 {
735 alignment = CellAlignment.CENTER;
736 cellSize = CellSize.PREFERRED;
737 minSize = NumberWithUnit.ZERO;
738 weight = 0;
739 return this;
740 }
741
742 /**
743 * Returns a {@link CellConstraints} instance for the properties defined
744 * so far. After that all properties are reset, so that properties for a
745 * new instance can be specified.
746 *
747 * @return a {@code CellConstraints} instance corresponding to the
748 * properties set so far
749 * @throws IllegalStateException if required properties are missing
750 */
751 public CellConstraints create()
752 {
753 if (cellSize == null && minSize == null)
754 {
755 throw new IllegalStateException("Size of cell is unspecified!"
756 + " Set a cell size or a minimum size.");
757 }
758 if (cellSize == null)
759 {
760 cellSize = CellSize.NONE;
761 }
762 if (minSize == null)
763 {
764 minSize = NumberWithUnit.ZERO;
765 }
766
767 String spec = CellConstraints.toString(alignment, cellSize,
768 minSize, weight);
769 CellConstraints cc = cache.get(spec);
770 if (cc == null)
771 {
772 cc = new CellConstraints(alignment, cellSize, NumberWithUnit
773 .nonNull(minSize), weight);
774 cache.put(spec, cc);
775 }
776
777 reset();
778 return cc;
779 }
780
781 /**
782 * Resets all properties set so far. This reverts all invocations of
783 * {@code withXXXX()} methods since the last instance was created.
784 */
785 public void reset()
786 {
787 alignment = getDefaultAlignment();
788 cellSize = null;
789 minSize = null;
790 weight = 0;
791 }
792
793 /**
794 * Parses a string with the specification of a {@code CellConstraints}
795 * object and returns a corresponding instance.
796 *
797 * @param spec the specification of the {@code CellConstraints} instance
798 * @return the corresponding {@code CellConstraints} instance
799 * @throws IllegalArgumentException if the string cannot be parsed
800 */
801 public CellConstraints fromString(String spec)
802 {
803 CellConstraints cc = cache.get(spec);
804 if (cc != null)
805 {
806 return cc;
807 }
808
809 cc = CellConstraints.parse(spec, getDefaultAlignment());
810 cache.put(spec, cc);
811 String canonicalSpec = cc.toSpecificationString();
812 if (!canonicalSpec.equals(spec))
813 {
814 cache.put(canonicalSpec, cc);
815 }
816
817 return cc;
818 }
819 }
820 }