1 package net.miginfocom.layout;
3 import java.util.ArrayList;
4 import java.util.HashMap;
5 import java.util.Iterator;
11 * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (miglayout (at) miginfocom (dot) com)
12 * All rights reserved.
14 * Redistribution and use in source and binary forms, with or without modification,
15 * are permitted provided that the following conditions are met:
16 * Redistributions of source code must retain the above copyright notice, this list
17 * of conditions and the following disclaimer.
18 * Redistributions in binary form must reproduce the above copyright notice, this
19 * list of conditions and the following disclaimer in the documentation and/or other
20 * materials provided with the distribution.
21 * Neither the name of the MiG InfoCom AB nor the names of its contributors may be
22 * used to endorse or promote products derived from this software without specific
23 * prior written permission.
25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
26 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
27 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
28 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
29 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
31 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
32 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
37 * @author Mikael Grev, MiG InfoCom AB
41 /** Parses string constraints.
43 public final class ConstraintParser
45 private ConstraintParser()
49 /** Parses the layout constraints and stores the parsed values in the transient (cache) member variables.
50 * @param s The String to parse. Should not be <code>null</code> and <b>must be lower case and trimmed</b>.
51 * @throws RuntimeException if the constraint was not valid.
52 * @return The parsed constraint. Never <code>null</code>.
54 public static LC parseLayoutConstraint(String s)
60 String[] parts = toTrimmedTokens(s, ',');
62 // First check for "ltr" or "rtl" since that will affect the interpretation of the other constraints.
63 for (int i = 0; i < parts.length; i++) {
64 String part = parts[i];
68 int len = part.length();
69 if (len == 3 || len == 11) { // Optimization
70 if (part.equals("ltr") || part.equals("rtl") || part.equals("lefttoright") || part.equals("righttoleft")) {
71 lc.setLeftToRight(part.charAt(0) == 'l' ? Boolean.TRUE : Boolean.FALSE);
72 parts[i] = null; // So we will not try to interpret it again
75 if (part.equals("ttb") || part.equals("btt") || part.equals("toptobottom") || part.equals("bottomtotop")) {
76 lc.setTopToBottom(part.charAt(0) == 't');
77 parts[i] = null; // So we will not try to interpret it again
82 for (String part : parts) {
83 if (part == null || part.length() == 0)
88 char c = part.charAt(0);
90 if (c == 'w' || c == 'h') {
92 ix = startsWithLenient(part, "wrap", -1, true);
94 String num = part.substring(ix).trim();
95 lc.setWrapAfter(num.length() != 0 ? Integer.parseInt(num) : 0);
99 boolean isHor = c == 'w';
100 if (isHor && (part.startsWith("w ") || part.startsWith("width "))) {
101 String sz = part.substring(part.charAt(1) == ' ' ? 2 : 6).trim();
102 lc.setWidth(parseBoundSize(sz, false, true));
106 if (!isHor && (part.startsWith("h ") || part.startsWith("height "))) {
107 String uvStr = part.substring(part.charAt(1) == ' ' ? 2 : 7).trim();
108 lc.setHeight(parseBoundSize(uvStr, false, false));
112 if (part.length() > 5) {
113 String sz = part.substring(5).trim();
114 if (part.startsWith("wmin ")) {
117 } else if (part.startsWith("wmax ")) {
120 } else if (part.startsWith("hmin ")) {
123 } else if (part.startsWith("hmax ")) {
129 if (part.startsWith("hidemode ")) {
130 lc.setHideMode(Integer.parseInt(part.substring(9)));
136 if (part.startsWith("gapx ")) {
137 lc.setGridGapX(parseBoundSize(part.substring(5).trim(), true, true));
141 if (part.startsWith("gapy ")) {
142 lc.setGridGapY(parseBoundSize(part.substring(5).trim(), true, false));
146 if (part.startsWith("gap ")) {
147 String[] gaps = toTrimmedTokens(part.substring(4).trim(), ' ');
148 lc.setGridGapX(parseBoundSize(gaps[0], true, true));
149 lc.setGridGapY(gaps.length > 1 ? parseBoundSize(gaps[1], true, false) : lc.getGridGapX());
155 ix = startsWithLenient(part, "debug", 5, true);
157 String millis = part.substring(ix).trim();
158 lc.setDebugMillis(millis.length() > 0 ? Integer.parseInt(millis) : 1000);
164 if (part.equals("nogrid")) {
169 if (part.equals("nocache")) {
174 if (part.equals("novisualpadding")) {
175 lc.setVisualPadding(false);
181 if (part.equals("fill") || part.equals("fillx") || part.equals("filly")) {
182 lc.setFillX(part.length() == 4 || part.charAt(4) == 'x');
183 lc.setFillY(part.length() == 4 || part.charAt(4) == 'y');
187 if (part.equals("flowy")) {
192 if (part.equals("flowx")) {
193 lc.setFlowX(true); // This is the default but added for consistency
199 ix = startsWithLenient(part, "insets", 3, true);
201 String insStr = part.substring(ix).trim();
202 UnitValue[] ins = parseInsets(insStr, true);
203 LayoutUtil.putCCString(ins, insStr);
210 ix = startsWithLenient(part, new String[]{"aligny", "ay"}, new int[]{6, 2}, true);
212 UnitValue align = parseUnitValueOrAlign(part.substring(ix).trim(), false, null);
213 if (align == UnitValue.BASELINE_IDENTITY)
214 throw new IllegalArgumentException("'baseline' can not be used to align the whole component group.");
219 ix = startsWithLenient(part, new String[]{"alignx", "ax"}, new int[]{6, 2}, true);
221 lc.setAlignX(parseUnitValueOrAlign(part.substring(ix).trim(), true, null));
225 ix = startsWithLenient(part, "align", 2, true);
227 String[] gaps = toTrimmedTokens(part.substring(ix).trim(), ' ');
228 lc.setAlignX(parseUnitValueOrAlign(gaps[0], true, null));
229 if (gaps.length > 1) {
230 UnitValue align = parseUnitValueOrAlign(gaps[1], false, null);
231 if (align == UnitValue.BASELINE_IDENTITY)
232 throw new IllegalArgumentException("'baseline' can not be used to align the whole component group.");
240 if (part.startsWith("packalign ")) {
241 String[] packs = toTrimmedTokens(part.substring(10).trim(), ' ');
242 lc.setPackWidthAlign(packs[0].length() > 0 ? Float.parseFloat(packs[0]) : 0.5f);
243 if (packs.length > 1)
244 lc.setPackHeightAlign(Float.parseFloat(packs[1]));
248 if (part.startsWith("pack ") || part.equals("pack")) {
249 String ps = part.substring(4).trim();
250 String[] packs = toTrimmedTokens(ps.length() > 0 ? ps : "pref pref", ' ');
251 lc.setPackWidth(parseBoundSize(packs[0], false, true));
252 if (packs.length > 1)
253 lc.setPackHeight(parseBoundSize(packs[1], false, false));
259 if (lc.getAlignX() == null) {
260 UnitValue alignX = parseAlignKeywords(part, true);
261 if (alignX != null) {
262 lc.setAlignX(alignX);
267 UnitValue alignY = parseAlignKeywords(part, false);
268 if (alignY != null) {
269 lc.setAlignY(alignY);
273 throw new IllegalArgumentException("Unknown Constraint: '" + part + "'\n");
275 } catch (Exception ex) {
276 throw new IllegalArgumentException("Illegal Constraint: '" + part + "'\n" + ex.getMessage());
280 // lc = (LC) serializeTest(lc);
285 /** Parses the column or rows constraints. They normally looks something like <code>"[min:pref]rel[10px][]"</code>.
286 * @param s The string to parse. Not <code>null</code>.
287 * @return An array of {@link DimConstraint}s that is as many are there exist "[...]" sections in the string that is parsed.
288 * @throws RuntimeException if the constraint was not valid.
290 public static AC parseRowConstraints(String s)
292 return parseAxisConstraint(s, false);
295 /** Parses the column or rows constraints. They normally looks something like <code>"[min:pref]rel[10px][]"</code>.
296 * @param s The string to parse. Not <code>null</code>.
297 * @return An array of {@link DimConstraint}s that is as many are there exist "[...]" sections in the string that is parsed.
298 * @throws RuntimeException if the constraint was not valid.
300 public static AC parseColumnConstraints(String s)
302 return parseAxisConstraint(s, true);
305 /** Parses the column or rows constraints. They normally looks something like <code>"[min:pref]rel[10px][]"</code>.
306 * @param s The string to parse. Not <code>null</code>.
307 * @param isCols If this for columns rather than rows.
308 * @return An array of {@link DimConstraint}s that is as many are there exist "[...]" sections in the string that is parsed.
309 * @throws RuntimeException if the constraint was not valid.
311 private static AC parseAxisConstraint(String s, boolean isCols)
316 return new AC(); // Short circuit for performance.
320 ArrayList<String> parts = getRowColAndGapsTrimmed(s);
322 BoundSize[] gaps = new BoundSize[(parts.size() >> 1) + 1];
323 for (int i = 0, iSz = parts.size(), gIx = 0; i < iSz; i += 2, gIx++)
324 gaps[gIx] = parseBoundSize(parts.get(i), true, isCols);
326 DimConstraint[] colSpecs = new DimConstraint[parts.size() >> 1];
327 for (int i = 0, gIx = 0; i < colSpecs.length; i++, gIx++) {
328 if (gIx >= gaps.length - 1)
329 gIx = gaps.length - 2;
331 colSpecs[i] = parseDimConstraint(parts.get((i << 1) + 1), gaps[gIx], gaps[gIx + 1], isCols);
335 ac.setConstaints(colSpecs);
337 // ac = (AC) serializeTest(ac);
342 /** Parses a single column or row constraint.
343 * @param s The single constraint to parse. May look something like <code>"min:pref,fill,grow"</code>. Should not be <code>null</code> and <b>must
344 * be lower case and trimmed</b>.
345 * @param gapBefore The default gap "before" the column/row constraint. Can be overridden with a <code>"gap"</code> section within <code>s</code>.
346 * @param gapAfter The default gap "after" the column/row constraint. Can be overridden with a <code>"gap"</code> section within <code>s</code>.
347 * @param isCols If the constraints are column constraints rather than row constraints.
348 * @return A single constraint. Never <code>null</code>.
349 * @throws RuntimeException if the constraint was not valid.
351 private static DimConstraint parseDimConstraint(String s, BoundSize gapBefore, BoundSize gapAfter, boolean isCols)
353 DimConstraint dimConstraint = new DimConstraint();
356 dimConstraint.setGapBefore(gapBefore);
357 dimConstraint.setGapAfter(gapAfter);
359 String[] parts = toTrimmedTokens(s, ',');
360 for (int i = 0; i < parts.length; i++) {
361 String part = parts[i];
363 if (part.length() == 0)
366 if (part.equals("fill")) {
367 dimConstraint.setFill(true);
368 // dimConstraint.setAlign(null); // Can not have both fill and alignment (changed for 3.5 since it can have "growy 0")
372 if (part.equals("nogrid")) {
373 dimConstraint.setNoGrid(true);
378 char c = part.charAt(0);
381 ix = startsWithLenient(part, new String[] {"sizegroup", "sg"}, new int[] {5, 2}, true);
383 dimConstraint.setSizeGroup(part.substring(ix).trim());
388 ix = startsWithLenient(part, new String[] {"shrinkprio", "shp"}, new int[] {10, 3}, true);
390 dimConstraint.setShrinkPriority(Integer.parseInt(part.substring(ix).trim()));
394 ix = startsWithLenient(part, "shrink", 6, true);
396 dimConstraint.setShrink(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
402 ix = startsWithLenient(part, new String[] {"growpriority", "gp"}, new int[] {5, 2}, true);
404 dimConstraint.setGrowPriority(Integer.parseInt(part.substring(ix).trim()));
408 ix = startsWithLenient(part, "grow", 4, true);
410 dimConstraint.setGrow(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
416 ix = startsWithLenient(part, "align", 2, true);
418 // if (dimConstraint.isFill() == false) // Swallow, but ignore if fill is set. (changed for 3.5 since it can have "growy 0")
419 dimConstraint.setAlign(parseUnitValueOrAlign(part.substring(ix).trim(), isCols, null));
424 UnitValue align = parseAlignKeywords(part, isCols);
426 // if (dimConstraint.isFill() == false) // Swallow, but ignore if fill is set. (changed for 3.5 since it can have "growy 0")
427 dimConstraint.setAlign(align);
431 // Only min:pref:max still left that is ok
432 dimConstraint.setSize(parseBoundSize(part, false, isCols));
434 } catch (Exception ex) {
435 throw new IllegalArgumentException("Illegal constraint: '" + part + "'\n" + ex.getMessage());
438 return dimConstraint;
441 /** Parses all component constraints and stores the parsed values in the transient (cache) member variables.
442 * @param constrMap The constraints as <code>String</code>s. Strings <b>must be lower case and trimmed</b>
443 * @return The parsed constraints. Never <code>null</code>.
445 public static Map<ComponentWrapper, CC> parseComponentConstraints(Map<ComponentWrapper, String> constrMap)
447 HashMap<ComponentWrapper, CC> flowConstrMap = new HashMap<ComponentWrapper, CC>();
449 for (Iterator<Map.Entry<ComponentWrapper, String>> it = constrMap.entrySet().iterator(); it.hasNext();) {
450 Map.Entry<ComponentWrapper, String> entry = it.next();
451 flowConstrMap.put(entry.getKey(), parseComponentConstraint(entry.getValue()));
454 return flowConstrMap;
457 /** Parses one component constraint and returns the parsed value.
458 * @param s The string to parse. <b>Must be lower case and trimmed</b>.
459 * @throws RuntimeException if the constraint was not valid.
460 * @return The parsed constraint. Never <code>null</code>.
462 public static CC parseComponentConstraint(String s)
466 if (s == null || s.isEmpty())
469 String[] parts = toTrimmedTokens(s, ',');
471 for (String part : parts) {
473 if (part.length() == 0)
477 char c = part.charAt(0);
480 if (part.equals("north")) {
485 if (part.equals("newline")) {
490 if (part.startsWith("newline ")) {
491 String gapSz = part.substring(7).trim();
492 cc.setNewlineGapSize(parseBoundSize(gapSz, true, true));
497 if (c == 'f' && (part.equals("flowy") || part.equals("flowx"))) {
498 cc.setFlowX(part.charAt(4) == 'x' ? Boolean.TRUE : Boolean.FALSE);
503 ix = startsWithLenient(part, "skip", 4, true);
505 String num = part.substring(ix).trim();
506 cc.setSkip(num.length() != 0 ? Integer.parseInt(num) : 1);
510 ix = startsWithLenient(part, "split", 5, true);
512 String split = part.substring(ix).trim();
513 cc.setSplit(split.length() > 0 ? Integer.parseInt(split) : LayoutUtil.INF);
517 if (part.equals("south")) {
522 ix = startsWithLenient(part, new String[]{"spany", "sy"}, new int[]{5, 2}, true);
524 cc.setSpanY(parseSpan(part.substring(ix).trim()));
528 ix = startsWithLenient(part, new String[]{"spanx", "sx"}, new int[]{5, 2}, true);
530 cc.setSpanX(parseSpan(part.substring(ix).trim()));
534 ix = startsWithLenient(part, "span", 4, true);
536 String[] spans = toTrimmedTokens(part.substring(ix).trim(), ' ');
537 cc.setSpanX(spans[0].length() > 0 ? Integer.parseInt(spans[0]) : LayoutUtil.INF);
538 cc.setSpanY(spans.length > 1 ? Integer.parseInt(spans[1]) : 1);
542 ix = startsWithLenient(part, "shrinkx", 7, true);
544 cc.getHorizontal().setShrink(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
548 ix = startsWithLenient(part, "shrinky", 7, true);
550 cc.getVertical().setShrink(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
554 ix = startsWithLenient(part, "shrink", 6, false);
556 String[] shrinks = toTrimmedTokens(part.substring(ix).trim(), ' ');
557 cc.getHorizontal().setShrink(parseFloat(shrinks[0], ResizeConstraint.WEIGHT_100));
558 if (shrinks.length > 1)
559 cc.getVertical().setShrink(parseFloat(shrinks[1], ResizeConstraint.WEIGHT_100));
563 ix = startsWithLenient(part, new String[]{"shrinkprio", "shp"}, new int[]{10, 3}, true);
565 String sp = part.substring(ix).trim();
566 if (sp.startsWith("x") || sp.startsWith("y")) { // To handle "gpx", "gpy", "shrinkpriorityx", shrinkpriorityy"
567 (sp.startsWith("x") ? cc.getHorizontal() : cc.getVertical()).setShrinkPriority(Integer.parseInt(sp.substring(2)));
569 String[] shrinks = toTrimmedTokens(sp, ' ');
570 cc.getHorizontal().setShrinkPriority(Integer.parseInt(shrinks[0]));
571 if (shrinks.length > 1)
572 cc.getVertical().setShrinkPriority(Integer.parseInt(shrinks[1]));
577 ix = startsWithLenient(part, new String[]{"sizegroupx", "sizegroupy", "sgx", "sgy"}, new int[]{9, 9, 2, 2}, true);
579 String sg = part.substring(ix).trim();
580 char lc = part.charAt(ix - 1);
582 cc.getHorizontal().setSizeGroup(sg);
584 cc.getVertical().setSizeGroup(sg);
590 ix = startsWithLenient(part, "growx", 5, true);
592 cc.getHorizontal().setGrow(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
596 ix = startsWithLenient(part, "growy", 5, true);
598 cc.getVertical().setGrow(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
602 ix = startsWithLenient(part, "grow", 4, false);
604 String[] grows = toTrimmedTokens(part.substring(ix).trim(), ' ');
605 cc.getHorizontal().setGrow(parseFloat(grows[0], ResizeConstraint.WEIGHT_100));
606 cc.getVertical().setGrow(parseFloat(grows.length > 1 ? grows[1] : "", ResizeConstraint.WEIGHT_100));
610 ix = startsWithLenient(part, new String[]{"growprio", "gp"}, new int[]{8, 2}, true);
612 String gp = part.substring(ix).trim();
613 char c0 = gp.length() > 0 ? gp.charAt(0) : ' ';
614 if (c0 == 'x' || c0 == 'y') { // To handle "gpx", "gpy", "growpriorityx", growpriorityy"
615 (c0 == 'x' ? cc.getHorizontal() : cc.getVertical()).setGrowPriority(Integer.parseInt(gp.substring(2)));
617 String[] grows = toTrimmedTokens(gp, ' ');
618 cc.getHorizontal().setGrowPriority(Integer.parseInt(grows[0]));
619 if (grows.length > 1)
620 cc.getVertical().setGrowPriority(Integer.parseInt(grows[1]));
625 if (part.startsWith("gap")) {
626 BoundSize[] gaps = parseGaps(part); // Changes order!!
628 cc.getVertical().setGapBefore(gaps[0]);
630 cc.getHorizontal().setGapBefore(gaps[1]);
632 cc.getVertical().setGapAfter(gaps[2]);
634 cc.getHorizontal().setGapAfter(gaps[3]);
640 ix = startsWithLenient(part, new String[]{"aligny", "ay"}, new int[]{6, 2}, true);
642 cc.getVertical().setAlign(parseUnitValueOrAlign(part.substring(ix).trim(), false, null));
646 ix = startsWithLenient(part, new String[]{"alignx", "ax"}, new int[]{6, 2}, true);
648 cc.getHorizontal().setAlign(parseUnitValueOrAlign(part.substring(ix).trim(), true, null));
652 ix = startsWithLenient(part, "align", 2, true);
654 String[] gaps = toTrimmedTokens(part.substring(ix).trim(), ' ');
655 cc.getHorizontal().setAlign(parseUnitValueOrAlign(gaps[0], true, null));
657 cc.getVertical().setAlign(parseUnitValueOrAlign(gaps[1], false, null));
662 if ((c == 'x' || c == 'y') && part.length() > 2) {
663 char c2 = part.charAt(1);
664 if (c2 == ' ' || (c2 == '2' && part.charAt(2) == ' ')) {
665 if (cc.getPos() == null) {
666 cc.setPos(new UnitValue[4]);
667 } else if (cc.isBoundsInGrid() == false) {
668 throw new IllegalArgumentException("Cannot combine 'position' with 'x/y/x2/y2' keywords.");
671 int edge = (c == 'x' ? 0 : 1) + (c2 == '2' ? 2 : 0);
672 UnitValue[] pos = cc.getPos();
673 pos[edge] = parseUnitValue(part.substring(2).trim(), null, c == 'x');
675 cc.setBoundsInGrid(true);
681 ix = startsWithLenient(part, "cell", 4, true);
683 String[] grs = toTrimmedTokens(part.substring(ix).trim(), ' ');
685 throw new IllegalArgumentException("At least two integers must follow " + part);
686 cc.setCellX(Integer.parseInt(grs[0]));
687 cc.setCellY(Integer.parseInt(grs[1]));
689 cc.setSpanX(Integer.parseInt(grs[2]));
691 cc.setSpanY(Integer.parseInt(grs[3]));
697 ix = startsWithLenient(part, "pos", 3, true);
699 if (cc.getPos() != null && cc.isBoundsInGrid())
700 throw new IllegalArgumentException("Can not combine 'pos' with 'x/y/x2/y2' keywords.");
702 String[] pos = toTrimmedTokens(part.substring(ix).trim(), ' ');
703 UnitValue[] bounds = new UnitValue[4];
704 for (int j = 0; j < pos.length; j++)
705 bounds[j] = parseUnitValue(pos[j], null, j % 2 == 0);
707 if (bounds[0] == null && bounds[2] == null || bounds[1] == null && bounds[3] == null)
708 throw new IllegalArgumentException("Both x and x2 or y and y2 can not be null!");
711 cc.setBoundsInGrid(false);
715 ix = startsWithLenient(part, "pad", 3, true);
717 UnitValue[] p = parseInsets(part.substring(ix).trim(), false);
718 cc.setPadding(new UnitValue[]{
720 p.length > 1 ? p[1] : null,
721 p.length > 2 ? p[2] : null,
722 p.length > 3 ? p[3] : null});
726 ix = startsWithLenient(part, "pushx", 5, true);
728 cc.setPushX(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
732 ix = startsWithLenient(part, "pushy", 5, true);
734 cc.setPushY(parseFloat(part.substring(ix).trim(), ResizeConstraint.WEIGHT_100));
738 ix = startsWithLenient(part, "push", 4, false);
740 String[] pushs = toTrimmedTokens(part.substring(ix).trim(), ' ');
741 cc.setPushX(parseFloat(pushs[0], ResizeConstraint.WEIGHT_100));
742 cc.setPushY(parseFloat(pushs.length > 1 ? pushs[1] : "", ResizeConstraint.WEIGHT_100));
748 ix = startsWithLenient(part, "tag", 3, true);
750 cc.setTag(part.substring(ix).trim());
755 if (c == 'w' || c == 'h') {
756 if (part.equals("wrap")) {
761 if (part.startsWith("wrap ")) {
762 String gapSz = part.substring(5).trim();
763 cc.setWrapGapSize(parseBoundSize(gapSz, true, true));
767 boolean isHor = c == 'w';
768 if (isHor && (part.startsWith("w ") || part.startsWith("width "))) {
769 String uvStr = part.substring(part.charAt(1) == ' ' ? 2 : 6).trim();
770 cc.getHorizontal().setSize(parseBoundSize(uvStr, false, true));
774 if (!isHor && (part.startsWith("h ") || part.startsWith("height "))) {
775 String uvStr = part.substring(part.charAt(1) == ' ' ? 2 : 7).trim();
776 cc.getVertical().setSize(parseBoundSize(uvStr, false, false));
780 if (part.startsWith("wmin ") || part.startsWith("wmax ") || part.startsWith("hmin ") || part.startsWith("hmax ")) {
781 String uvStr = part.substring(5).trim();
782 if (uvStr.length() > 0) {
783 UnitValue uv = parseUnitValue(uvStr, null, isHor);
784 boolean isMin = part.charAt(3) == 'n';
785 DimConstraint dc = isHor ? cc.getHorizontal() : cc.getVertical();
786 dc.setSize(new BoundSize(
787 isMin ? uv : dc.getSize().getMin(),
788 dc.getSize().getPreferred(),
789 isMin ? (dc.getSize().getMax()) : uv,
796 if (part.equals("west")) {
801 if (part.startsWith("hidemode ")) {
802 cc.setHideMode(Integer.parseInt(part.substring(9)));
807 if (c == 'i' && part.startsWith("id ")) {
808 cc.setId(part.substring(3).trim());
809 int dIx = cc.getId().indexOf('.');
810 if (dIx == 0 || dIx == cc.getId().length() - 1)
811 throw new IllegalArgumentException("Dot must not be first or last!");
817 if (part.equals("east")) {
822 if (part.equals("external")) {
823 cc.setExternal(true);
827 ix = startsWithLenient(part, new String[]{"endgroupx", "endgroupy", "egx", "egy"}, new int[]{-1, -1, -1, -1}, true);
829 String sg = part.substring(ix).trim();
830 char lc = part.charAt(ix - 1);
831 DimConstraint dc = (lc == 'x' ? cc.getHorizontal() : cc.getVertical());
838 if (part.equals("dock north")) {
842 if (part.equals("dock west")) {
846 if (part.equals("dock south")) {
850 if (part.equals("dock east")) {
855 if (part.equals("dock center")) {
856 cc.getHorizontal().setGrow(100f);
857 cc.getVertical().setGrow(100f);
865 ix = startsWithLenient(part, new String[] {"visualpadding", "vp"}, new int[] {3, 2}, true);
867 UnitValue[] p = parseInsets(part.substring(ix).trim(), false);
868 cc.setVisualPadding(new UnitValue[] {
870 p.length > 1 ? p[1] : null,
871 p.length > 2 ? p[2] : null,
872 p.length > 3 ? p[3] : null});
877 UnitValue horAlign = parseAlignKeywords(part, true);
878 if (horAlign != null) {
879 cc.getHorizontal().setAlign(horAlign);
883 UnitValue verAlign = parseAlignKeywords(part, false);
884 if (verAlign != null) {
885 cc.getVertical().setAlign(verAlign);
889 throw new IllegalArgumentException("Unknown keyword.");
891 } catch (Exception ex) {
892 throw new IllegalArgumentException("Error parsing Constraint: '" + part + "'", ex);
896 // cc = (CC) serializeTest(cc);
901 /** Parses insets which consists of 1-4 <code>UnitValue</code>s.
902 * @param s The string to parse. E.g. "10 10 10 10" or "20". If less than 4 groups the last will be used for the missing.
903 * @param acceptPanel If "panel" and "dialog" should be accepted. They are used to access platform defaults.
904 * @return An array of length 4 with the parsed insets.
905 * @throws IllegalArgumentException if the parsing could not be done.
907 public static UnitValue[] parseInsets(String s, boolean acceptPanel)
909 if (s.length() == 0 || s.equals("dialog") || s.equals("panel")) {
910 if (acceptPanel == false)
911 throw new IllegalArgumentException("Insets now allowed: " + s + "\n");
913 boolean isPanel = s.startsWith("p");
914 UnitValue[] ins = new UnitValue[4];
915 for (int j = 0; j < 4; j++)
916 ins[j] = isPanel ? PlatformDefaults.getPanelInsets(j) : PlatformDefaults.getDialogInsets(j);
920 String[] insS = toTrimmedTokens(s, ' ');
921 UnitValue[] ins = new UnitValue[4];
922 for (int j = 0; j < 4; j++) {
923 UnitValue insSz = parseUnitValue(insS[j < insS.length ? j : insS.length - 1], UnitValue.ZERO, j % 2 == 1);
924 ins[j] = insSz != null ? insSz : PlatformDefaults.getPanelInsets(j);
931 * @param s The string that contains gap information. Should start with "gap".
932 * @return The gaps as specified in <code>s</code>. Indexed: <code>[top,left,bottom,right][min,pref,max]</code> or
933 * [before,after][min,pref,max] if <code>oneDim</code> is true.
935 private static BoundSize[] parseGaps(String s)
937 BoundSize[] ret = new BoundSize[4];
939 int ix = startsWithLenient(s, "gaptop", -1, true);
941 s = s.substring(ix).trim();
942 ret[0] = parseBoundSize(s, true, false);
946 ix = startsWithLenient(s, "gapleft", -1, true);
948 s = s.substring(ix).trim();
949 ret[1] = parseBoundSize(s, true, true);
953 ix = startsWithLenient(s, "gapbottom", -1, true);
955 s = s.substring(ix).trim();
956 ret[2] = parseBoundSize(s, true, false);
960 ix = startsWithLenient(s, "gapright", -1, true);
962 s = s.substring(ix).trim();
963 ret[3] = parseBoundSize(s, true, true);
967 ix = startsWithLenient(s, "gapbefore", -1, true);
969 s = s.substring(ix).trim();
970 ret[1] = parseBoundSize(s, true, true);
974 ix = startsWithLenient(s, "gapafter", -1, true);
976 s = s.substring(ix).trim();
977 ret[3] = parseBoundSize(s, true, true);
981 ix = startsWithLenient(s, new String[] {"gapx", "gapy"}, null, true);
983 boolean x = s.charAt(3) == 'x';
984 String[] gaps = toTrimmedTokens(s.substring(ix).trim(), ' ');
985 ret[x ? 1 : 0] = parseBoundSize(gaps[0], true, x);
987 ret[x ? 3 : 2] = parseBoundSize(gaps[1], true, !x);
991 ix = startsWithLenient(s, "gap ", 1, true);
993 String[] gaps = toTrimmedTokens(s.substring(ix).trim(), ' ');
995 ret[1] = parseBoundSize(gaps[0], true, true); // left
996 if (gaps.length > 1) {
997 ret[3] = parseBoundSize(gaps[1], true, false); // right
998 if (gaps.length > 2) {
999 ret[0] = parseBoundSize(gaps[2], true, true); // top
1000 if (gaps.length > 3)
1001 ret[2] = parseBoundSize(gaps[3], true, false); // bottom
1007 throw new IllegalArgumentException("Unknown Gap part: '" + s + "'");
1010 private static int parseSpan(String s)
1012 return s.length() > 0 ? Integer.parseInt(s) : LayoutUtil.INF;
1015 private static Float parseFloat(String s, Float nullVal)
1017 return s.length() > 0 ? new Float(Float.parseFloat(s)) : nullVal;
1020 /** Parses a single "min:pref:max" value. May look something like <code>"10px:20lp:30%"</code> or <code>"pref!"</code>.
1021 * @param s The string to parse. Not <code>null</code>.
1022 * @param isGap If this bound size is a gap (different empty string handling).
1023 * @param isHor If the size is for the horizontal dimension.
1024 * @return A bound size that may be <code>null</code> if the string was "null", "n" or <code>null</code>.
1026 public static BoundSize parseBoundSize(String s, boolean isGap, boolean isHor)
1028 if (s.length() == 0 || s.equals("null") || s.equals("n"))
1032 boolean push = false;
1033 if (s.endsWith("push")) {
1036 s = s.substring(0, l - (s.endsWith(":push") ? 5 : 4));
1037 if (s.length() == 0)
1038 return new BoundSize(null, null, null, true, cs);
1041 String[] sizes = toTrimmedTokens(s, ':');
1042 String s0 = sizes[0];
1044 if (sizes.length == 1) {
1045 boolean hasEM = s0.endsWith("!");
1047 s0 = s0.substring(0, s0.length() - 1);
1048 UnitValue uv = parseUnitValue(s0, null, isHor);
1049 return new BoundSize(((isGap || hasEM) ? uv : null), uv, (hasEM ? uv : null), push, cs);
1051 } else if (sizes.length == 2) {
1052 return new BoundSize(parseUnitValue(s0, null, isHor), parseUnitValue(sizes[1], null, isHor), null, push, cs);
1053 } else if (sizes.length == 3) {
1054 return new BoundSize(parseUnitValue(s0, null, isHor), parseUnitValue(sizes[1], null, isHor), parseUnitValue(sizes[2], null, isHor), push, cs);
1056 throw new IllegalArgumentException("Min:Preferred:Max size section must contain 0, 1 or 2 colons. '" + cs + "'");
1060 /** Parses a single unit value that may also be an alignment as parsed by {@link #parseAlignKeywords(String, boolean)}.
1061 * @param s The string to parse. Not <code>null</code>. May look something like <code>"10px"</code> or <code>"5dlu"</code>.
1062 * @param isHor If the value is for the horizontal dimension.
1063 * @param emptyReplacement A replacement if <code>s</code> is empty. May be <code>null</code>.
1064 * @return The parsed unit value. May be <code>null</code>.
1066 public static UnitValue parseUnitValueOrAlign(String s, boolean isHor, UnitValue emptyReplacement)
1068 if (s.length() == 0)
1069 return emptyReplacement;
1071 UnitValue align = parseAlignKeywords(s, isHor);
1075 return parseUnitValue(s, emptyReplacement, isHor);
1078 /** Parses a single unit value. E.g. "10px" or "5in"
1079 * @param s The string to parse. Not <code>null</code>. May look something like <code>"10px"</code> or <code>"5dlu"</code>.
1080 * @param isHor If the value is for the horizontal dimension.
1081 * @return The parsed unit value. <code>null</code> is empty string,
1083 public static UnitValue parseUnitValue(String s, boolean isHor)
1085 return parseUnitValue(s, null, isHor);
1088 /** Parses a single unit value.
1089 * @param s The string to parse. May be <code>null</code>. May look something like <code>"10px"</code> or <code>"5dlu"</code>.
1090 * @param emptyReplacement A replacement <code>s</code> is empty or <code>null</code>. May be <code>null</code>.
1091 * @param isHor If the value is for the horizontal dimension.
1092 * @return The parsed unit value. May be <code>null</code>.
1094 private static UnitValue parseUnitValue(String s, UnitValue emptyReplacement, boolean isHor)
1096 if (s == null || s.length() == 0)
1097 return emptyReplacement;
1099 String cs = s; // Save creation string.
1100 char c0 = s.charAt(0);
1102 // Remove start and end parentheses, if there.
1103 if (c0 == '(' && s.charAt(s.length() - 1) == ')')
1104 s = s.substring(1, s.length() - 1);
1106 if (c0 == 'n' && (s.equals("null") || s.equals("n")))
1109 if (c0 == 'i' && s.equals("inf"))
1110 return UnitValue.INF;
1112 int oper = getOper(s);
1113 boolean inline = oper == UnitValue.ADD || oper == UnitValue.SUB || oper == UnitValue.MUL || oper == UnitValue.DIV;
1115 if (oper != UnitValue.STATIC) { // It is a multi-value
1118 if (inline == false) { // If the format is of type "opr(xxx,yyy)" (compared to in-line "10%+15px")
1119 String sub = s.substring(4, s.length() - 1).trim();
1120 uvs = toTrimmedTokens(sub, ',');
1121 if (uvs.length == 1)
1122 return parseUnitValue(sub, null, isHor);
1125 if (oper == UnitValue.ADD) {
1127 } else if (oper == UnitValue.SUB) {
1129 } else if (oper == UnitValue.MUL) {
1131 } else { // div left
1134 uvs = toTrimmedTokens(s, delim);
1135 if (uvs.length > 2) { // More than one +-*/.
1136 String last = uvs[uvs.length - 1];
1137 String first = s.substring(0, s.length() - last.length() - 1);
1138 uvs = new String[] {first, last};
1142 if (uvs.length != 2)
1143 throw new IllegalArgumentException("Malformed UnitValue: '" + s + "'");
1145 UnitValue sub1 = parseUnitValue(uvs[0], null, isHor);
1146 UnitValue sub2 = parseUnitValue(uvs[1], null, isHor);
1148 if (sub1 == null || sub2 == null)
1149 throw new IllegalArgumentException("Malformed UnitValue. Must be two sub-values: '" + s + "'");
1151 return new UnitValue(isHor, oper, sub1, sub2, cs);
1154 String[] numParts = getNumTextParts(s);
1155 float value = numParts[0].length() > 0 ? Float.parseFloat(numParts[0]) : 1; // e.g. "related" has no number part..
1157 return new UnitValue(value, numParts[1], isHor, oper, cs);
1159 } catch(Exception e) {
1160 throw new IllegalArgumentException("Malformed UnitValue: '" + s + "'", e);
1165 /** Parses alignment keywords and returns the appropriate <code>UnitValue</code>.
1166 * @param s The string to parse. Not <code>null</code>.
1167 * @param isHor If alignments for horizontal is checked. <code>false</code> means vertical.
1168 * @return The unit value or <code>null</code> if not recognized (no exception).
1170 static UnitValue parseAlignKeywords(String s, boolean isHor)
1172 if (startsWithLenient(s, "center", 1, false) != -1)
1173 return UnitValue.CENTER;
1176 if (startsWithLenient(s, "left", 1, false) != -1)
1177 return UnitValue.LEFT;
1179 if (startsWithLenient(s, "right", 1, false) != -1)
1180 return UnitValue.RIGHT;
1182 if (startsWithLenient(s, "leading", 4, false) != -1)
1183 return UnitValue.LEADING;
1185 if (startsWithLenient(s, "trailing", 5, false) != -1)
1186 return UnitValue.TRAILING;
1188 if (startsWithLenient(s, "label", 5, false) != -1)
1189 return UnitValue.LABEL;
1193 if (startsWithLenient(s, "baseline", 4, false) != -1)
1194 return UnitValue.BASELINE_IDENTITY;
1196 if (startsWithLenient(s, "top", 1, false) != -1)
1197 return UnitValue.TOP;
1199 if (startsWithLenient(s, "bottom", 1, false) != -1)
1200 return UnitValue.BOTTOM;
1206 /** Splits a text-number combination such as "hello 10.0" into <code>{"hello", "10.0"}</code>.
1207 * @param s The string to split. Not <code>null</code>. Needs be be reasonably formatted since the method
1208 * only finds the first 0-9 or . and cuts the string in half there.
1209 * @return Always length 2 and no <code>null</code> elements. Elements are "" if no part found.
1211 private static String[] getNumTextParts(String s)
1213 for (int i = 0, iSz = s.length(); i < iSz; i++) {
1214 char c = s.charAt(i);
1216 throw new IllegalArgumentException("Space in UnitValue: '" + s + "'");
1218 if ((c < '0' || c > '9') && c != '.' && c != '-')
1219 return new String[] {s.substring(0, i).trim(), s.substring(i).trim()};
1221 return new String[] {s, ""};
1224 /** Returns the operation depending on the start character.
1225 * @param s The string to check. Not <code>null</code>.
1226 * @return E.g. UnitValue.ADD, UnitValue.SUB or UnitValue.STATIC. Returns negative value for in-line operations.
1228 private static int getOper(String s)
1230 int len = s.length();
1232 return UnitValue.STATIC;
1234 if (len > 5 && s.charAt(3) == '(' && s.charAt(len - 1) == ')') {
1235 if (s.startsWith("min("))
1236 return UnitValue.MIN;
1238 if (s.startsWith("max("))
1239 return UnitValue.MAX;
1241 if (s.startsWith("mid("))
1242 return UnitValue.MID;
1245 // Try in-line add/sub. E.g. "pref+10px".
1246 for (int j = 0; j < 2; j++) { // First +- then */ (precedence)
1247 for (int i = len - 1, p = 0; i > 0; i--) {
1248 char c = s.charAt(i);
1251 } else if (c == '(') {
1253 } else if (p == 0) {
1256 return UnitValue.ADD;
1258 return UnitValue.SUB;
1261 return UnitValue.MUL;
1263 return UnitValue.DIV;
1268 return UnitValue.STATIC;
1271 /** Returns if a string shares at least a specified numbers starting characters with a number of matches.
1273 * This method just exercise {@link #startsWithLenient(String, String, int, boolean)} with every one of
1274 * <code>matches</code> and <code>minChars</code>.
1275 * @param s The string to check. Not <code>null</code>.
1276 * @param matches A number of possible starts for <code>s</code>.
1277 * @param minChars The minimum number of characters to match for every element in <code>matches</code>. Needs
1278 * to be of same length as <code>matches</code>. Can be <code>null</code>.
1279 * @param acceptTrailing If after the required number of characters are matched on recognized characters that are not
1280 * in one of the the <code>matches</code> string should be accepted. For instance if "abczz" should be matched with
1281 * "abcdef" and min chars 3.
1282 * @return The index of the first unmatched character if <code>minChars</code> was reached or <code>-1</code> if a match was not
1285 private static int startsWithLenient(String s, String[] matches, int[] minChars, boolean acceptTrailing)
1287 for (int i = 0; i < matches.length; i++) {
1288 int minChar = minChars != null ? minChars[i] : -1;
1289 int ix = startsWithLenient(s, matches[i], minChar, acceptTrailing);
1296 /** Returns if a string shares at least a specified numbers starting characters with a match.
1297 * @param s The string to check. Not <code>null</code> and must be trimmed.
1298 * @param match The possible start for <code>s</code>. Not <code>null</code> and must be trimmed.
1299 * @param minChars The mimimum number of characters to match to <code>s</code> for it this to be considered a match. -1 means
1300 * the full length of <code>match</code>.
1301 * @param acceptTrailing If after the required number of charecters are matched unrecognized characters that are not
1302 * in one of the the <code>matches</code> string should be accepted. For instance if "abczz" should be matched with
1303 * "abcdef" and min chars 3.
1304 * @return The index of the first unmatched character if <code>minChars</code> was reached or <code>-1</code> if a match was not
1307 private static int startsWithLenient(String s, String match, int minChars, boolean acceptTrailing)
1309 if (s.charAt(0) != match.charAt(0)) // Fast sanity check.
1313 minChars = match.length();
1315 int sSz = s.length();
1319 int mSz = match.length();
1321 for (int mIx = 0; mIx < mSz; sIx++, mIx++) {
1322 while (sIx < sSz && (s.charAt(sIx) == ' ' || s.charAt(sIx) == '_')) // Disregard spaces and _
1325 if (sIx >= sSz || s.charAt(sIx) != match.charAt(mIx))
1326 return mIx >= minChars && (acceptTrailing || sIx >= sSz) && (sIx >= sSz || s.charAt(sIx - 1) == ' ') ? sIx : -1;
1328 return sIx >= sSz || acceptTrailing ||s.charAt(sIx) == ' ' ? sIx : -1;
1331 /** Parses a string and returns it in those parts of the string that are separated with a <code>sep</code> character.
1333 * separator characters within parentheses will not be counted or handled in any way, whatever the depth.
1335 * A space separator will be a hit to one or more spaces and thus not return empty strings.
1336 * @param s The string to parse. If it starts and/or ends with a <code>sep</code> the first and/or last element returned will be "". If
1337 * two <code>sep</code> are next to each other and empty element will be "between" the periods. The <code>sep</code> themselves will never be returned.
1338 * @param sep The separator char.
1339 * @return Those parts of the string that are separated with <code>sep</code>. Never null and at least of size 1
1340 * @since 6.7.2 Changed so more than one space in a row works as one space.
1342 private static String[] toTrimmedTokens(String s, char sep)
1344 int toks = 0, sSize = s.length();
1345 boolean disregardDoubles = sep == ' ';
1349 for(int i = 0; i < sSize; i++) {
1350 char c = s.charAt(i);
1353 } else if (c == ')') {
1355 } else if (p == 0 && c == sep) {
1357 while (disregardDoubles && i < sSize - 1 && s.charAt(i + 1) == ' ')
1361 throw new IllegalArgumentException("Unbalanced parentheses: '" + s + "'");
1364 throw new IllegalArgumentException("Unbalanced parentheses: '" + s + "'");
1367 return new String [] {s.trim()};
1369 String[] retArr = new String[toks + 1];
1371 int st = 0, pNr = 0;
1373 for (int i = 0; i < sSize; i++) {
1375 char c = s.charAt(i);
1378 } else if (c == ')') {
1380 } else if (p == 0 && c == sep) {
1381 retArr[pNr++] = s.substring(st, i).trim();
1383 while (disregardDoubles && i < sSize - 1 && s.charAt(i + 1) == ' ')
1388 retArr[pNr++] = s.substring(st, sSize).trim();
1392 /** Parses "AAA[BBB]CCC[DDD]EEE" into {"AAA", "BBB", "CCC", "DDD", "EEE", "FFF"}. Handles empty parts. Will always start and end outside
1393 * a [] block so that the number of returned elemets will always be uneven and at least of length 3.
1395 * "|" is interpreted as "][".
1396 * @param s The string. Might be "" but not null. Should be trimmed.
1397 * @return The string divided into elements. Never <code>null</code> and at least of length 3.
1398 * @throws IllegalArgumentException If a [] mismatch of some kind. (If not same [ as ] count or if the interleave.)
1400 private static ArrayList<String> getRowColAndGapsTrimmed(String s)
1402 if (s.indexOf('|') != -1)
1403 s = s.replaceAll("\\|", "][");
1405 ArrayList<String> retList = new ArrayList<String>(Math.max(s.length() >> 2 + 1, 3)); // Approx return length.
1406 int s0 = 0, s1 = 0; // '[' and ']' count.
1407 int st = 0; // Start of "next token to add".
1408 for (int i = 0, iSz = s.length(); i < iSz; i++) {
1409 char c = s.charAt(i);
1412 } else if (c == ']') {
1418 if (s0 != s1 && (s0 - 1) != s1)
1419 break; // Wrong [ or ] found. Break for throw.
1421 retList.add(s.substring(st, i).trim());
1425 throw new IllegalArgumentException("'[' and ']' mismatch in row/column format string: " + s);
1431 } else if (retList.size() % 2 == 0) {
1432 retList.add(s.substring(st, s.length()));
1438 /** Makes <code>null</code> "", trims and converts to lower case.
1439 * @param s The string
1442 public static String prepare(String s)
1444 return s != null ? s.trim().toLowerCase() : "";
1447 // /** Tests to serialize and deserialize the object with both XMLEncoder/Decoder and through Serializable
1448 // * @param o The object to serialize
1449 // * @return The same object after a tri through the process.
1451 // public static final Object serializeTest(Object o)
1454 // ByteArrayOutputStream barr = new ByteArrayOutputStream();
1455 // XMLEncoder enc = new XMLEncoder(barr);
1456 // enc.writeObject(o);
1459 // XMLDecoder dec = new XMLDecoder(new ByteArrayInputStream(barr.toByteArray()));
1460 // o = dec.readObject();
1462 // } catch (Exception e) {
1463 // e.printStackTrace();
1467 // ByteArrayOutputStream barr = new ByteArrayOutputStream();
1468 // ObjectOutputStream oos = new ObjectOutputStream(barr);
1469 // oos.writeObject(o);
1472 // ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
1473 // o = ois.readObject();
1475 // } catch (Exception e) {
1476 // e.printStackTrace();