Java tutorial
/******************************************************************************* * Copyright (C) 2006-2013 AITIA International, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package ai.aitia.meme.utils; import java.awt.Component; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.StringTokenizer; import java.util.Map.Entry; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import com.jgoodies.forms.builder.ButtonStackBuilder; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.forms.builder.PanelBuilder; import com.jgoodies.forms.debug.FormDebugPanel; import com.jgoodies.forms.factories.Borders; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.ConstantSize; import com.jgoodies.forms.layout.FormLayout; import com.jgoodies.forms.layout.RowSpec; import com.jgoodies.forms.layout.Sizes; /** * Utilities for using the JGoodies Forms package (forms-1.0.7.jar). * The documentation here provides only a few examples and assumes a basic * knowledge of FormLayout's HTML documentation. (I think it's enough to read * the "Quick start" and "Reference" parts (including Alignments, Constant Sizes, * Component Sizes, Bounded Sizes, Cols & Rows, Cell Constraints, Groups)) * <br> * History:<ul> * <li> 2007-03-28 Comments improved * <li> 2007-02-28 Initial version * </ul> */ public class FormsUtils { public static boolean isDebug = false; //public static boolean isDebug = true; //========================================================================= // buttonStack() public static final String BS_UNRELATED = ""; // unrelated buttons marker for buttonStack() public static final String BS_GLUE = "glue"; // glue marker for buttonStack() //------------------------------------------------------------------------- /** * Example:<pre> * buttonStack( jMoveUpButton, jMoveDownButton, FormsUtils.BS_UNRELATED, * jRemoveButton, jEditButton ).getPanel() * </pre> */ public static ButtonStackBuilder buttonStack(Object... args) { ButtonStackBuilder ans = new ButtonStackBuilder(); boolean addrel = false; for (int i = 0; i < args.length; ++i) { if (args[i] instanceof javax.swing.JComponent) { if (addrel) ans.addRelatedGap(); ans.addGridded((javax.swing.JComponent) args[i]); addrel = true; } else if (BS_UNRELATED.equals(args[i])) { ans.addUnrelatedGap(); addrel = false; } else if (BS_GLUE.equals(args[i])) { ans.addGlue(); addrel = false; } else { ans.addStrut(Sizes.constant(args[i].toString(), false)); addrel = false; } } return ans; } //========================================================================= // build() /** * Definition of gap length characters. Must contain constant lengths only (e.g. "4dlu"). */ public static java.util.HashMap<Character, String> gapLength = new java.util.HashMap<Character, String>(); static { // Factory default gap lengths // These characters can be used: ' ` - = ~ / & % $ \\ ^ * # > @ gapLength.put('`', "1dlu"); gapLength.put('\'', "2dlu"); gapLength.put('-', "4dlu"); gapLength.put('~', "6dlu"); gapLength.put('=', "8dlu"); gapLength.put('%', "10dlu"); gapLength.put('$', "15dlu"); gapLength.put('\\', "20dlu"); } //------------------------------------------------------------------------- /** * Returns a DefaultFormBuilder containing the specified components and layout. * @param cols This parameter corresponds to the <code>encodedColumnSpecs</code> * parameter of FormLayout's ctor. Besides the encoding defined by FormLayout, * the following extensions are also available: the characters defined in the * global <code>{@link #gapLength}</code> variable (hereafter: gap-characters) * can be used to insert gap-columns. Gap columns must not appear in the * cell-specification (explained below) and they're automatically included in * column spans. * Consecutive gap-characters are coalesced into 1 gap column by calculating * their cumulated pixel size. * @param rows A string describing general builder settings + cell-specification * + row/colum spans + row heights + row groups. See the examples. * The digits and underscores specify which component goes into which cell(s) * of the layout grid (cell-specification). There can be at most one character * for every (non-gap) column specified by <code>cols</code>. Rows must be * separated by the '|' character. Only underscores, digits and letters are * allowed in the cell-specification (space isn't). Underscore means that a * cell is empty. A digit/letter character refers to a component in the varargs * list: '0'..'9', 'A'..'Z', 'a'..'z' (in this order) denote the first 62 * components of the <code>args</code> list. Repeating the same digit specifies * the component's column span (and row span, if repeated in consecutive rows * in the same columns, like '3' in the example).<br> * After the cell-specification, but before the newline ('|') character * the row height and row grouping can also be specified. It must begin * with a space to separate it from the cell-specification. The row * height can be a gap-character (for constant heights only) or a string * that is interpreted by RowSpec.decodeSpecs(). If omitted, the height * spec. of the most recent row is inherited. Content rows inherit the * height of the previous content row, gap rows inherit the height of * the previous gap row. A row is a gap row if its cell-specification is * omitted.<br> * Row grouping forces equal heights to every member of the group. It * can be specified by "grp?" strings using any character in place of * '?' (except space and '|'. In the example 'grp1' uses '1'). Rows * using the same grouping character will be in the same group. * By default there're no groups. * <br> * General builder-settings can be specified at the beginning of the * string, enclosed in square brackets ([..]). (No space is allowed * after the closing ']'). This is intended for future extensions, too. * The list of available settings is described at the {@link Prop} * enumeration. Note that setting names are case-sensitive, and should * be separated by commas. * @param args List of components. Besides java.awt.Component objects, * the caller may use plain strings and instances of the {@link Separator} * class. Plain strings are used to create labels (with mnemonic-expansion). * Separator objects will create and add separators to the form. * Any of these objects may be followed optionally by a {@link CellConstraints}, * a {@link CellConstraints.Alignment} or a {@link CellInsets} object, * which overrides the cell's default alignment, can extend its row/column * span and adjust its insets.<br> * If the first element of <code>args</code> is a java.util.Map object, * it is treated as an additional mapping for gap-characters. This * overrides the default global mapping. Note that gap-characters can * help you to set up uniform spacing on your forms. For example, if * you use "-" as normal column-gap and "~" as normal row-gap, fine-tuning * the sizes of these gaps later is as easy as changing the mapping for "-" * and "~" — there's no need to update all the dlu sizes in all layouts. * @see * Example1: <pre> * build("6dlu, p, 6dlu, 50dlu, 6dlu", * "_0_1_ pref| 6dlu|" + * "_2_33 pref:grow(0.5) grp1||" + * "_4_33 grp1", * component0, component1, component2, component3, * component4, cellConstraintsForComponent4).getPanel() * </pre> * The same exaple with gap-characters: <pre> * build("~ p ~ 50dlu, 6dlu", * "01_ pref|~|" + * "233 pref:grow(0.5) grp1||" + * "433 grp1", * component0, component1, component2, component3, * component4, cellConstraintsForComponent4).getPanel() * </pre> * Example3 (only the second argument): <pre> * "[LineGapSize=6dlu, ParagraphGapSize=20dlu]_0_1||_2_3||_4_5" * </pre> * Note: this method can be used with no components and empty cell-specification, * too. In this case only a {@link DefaultFormBuilder} is created, configured * and returned. Its operations can then be used to append components to the form. */ @SuppressWarnings("unchecked") public static DefaultFormBuilder build(String cols, String rows, Object... args) { Context ctx = new Context(); // Parse column widths // int firstArg = 0; if (args.length > 0 && args[0] instanceof java.util.Map) { ctx.localGapSpec = (java.util.Map<Character, String>) args[0]; firstArg += 1; } StringBuilder colstmp = new StringBuilder(); ctx.contentCol = parseColumnWidths(colstmp, cols, ctx.localGapSpec, 0); // Parse the list of components (may include individual cell-constraints) // ctx.components = new ArrayList<Rec>(args.length); for (int i = firstArg; i < args.length; ++i) { Rec r = new Rec(args[i]); if (i + 1 < args.length) { if (args[i + 1] instanceof CellConstraints) { r.cc = (CellConstraints) args[++i]; r.useAlignment = true; } else if (args[i + 1] instanceof CellConstraints.Alignment) { CellConstraints.Alignment a = (CellConstraints.Alignment) args[++i]; if (a == CellConstraints.BOTTOM || a == CellConstraints.TOP) r.cc = new CellConstraints(1, 1, CellConstraints.DEFAULT, a); else if (a == CellConstraints.LEFT || a == CellConstraints.RIGHT) r.cc = new CellConstraints(1, 1, a, CellConstraints.DEFAULT); else if (a == CellConstraints.CENTER || a == CellConstraints.FILL) r.cc = new CellConstraints(1, 1, a, a); r.useAlignment = (r.cc != null); } else if (args[i + 1] instanceof CellInsets) { CellInsets ci = ((CellInsets) args[++i]); r.cc = ci.cc; r.useAlignment = ci.useAlignment; r.useInsets = true; //} //else if (args[i+1] == null) { // this would allow superfluous 'null' values // i += 1; } } ctx.components.add(r); } // Parse general settings (but don't apply yet) // EnumMap<Prop, Object> props = null; int i = rows.indexOf(']'); if (i >= 0) { String defaults = rows.substring(0, i); rows = rows.substring(++i); i = defaults.indexOf('['); ctx.input = defaults.substring(++i); props = Prop.parseGeneralSettings(ctx); } // Parse cell-specification, row heights and row groups // String cells[] = rows.split("\\|", -1); StringBuilder rowstmp = new StringBuilder(); java.util.HashMap<Character, int[]> rowGroups = new HashMap<Character, int[]>(); String lastContentRowHeight = "p", lastGapRowHeight = null; int rowcnt = 0; for (i = 0; i < cells.length; ++i) { rowcnt += 1; // See if it begins with a gap-character String g = (cells[i].length() > 0) ? getGap(cells[i].charAt(0), ctx.localGapSpec) : null; if (g != null) cells[i] = ' ' + cells[i]; int j = cells[i].indexOf(' '); boolean gapRow = (j == 0) || (cells[i].length() == 0); String rh = null; if (j >= 0) { String tmp[] = cells[i].substring(j + 1).split("\\s"); // expect height and grouping specifications cells[i] = cells[i].substring(0, j); ArrayList<String> gaps = new ArrayList<String>(); for (j = 0; j < tmp.length; ++j) { if (tmp[j].length() == 0) continue; if (tmp[j].length() == 4 && tmp[j].toLowerCase().startsWith("grp")) { Character groupch = tmp[j].charAt(3); rowGroups.put(groupch, appendIntArray(rowGroups.get(groupch), rowcnt)); } else { rh = tmp[j]; for (int k = 0, n = tmp[j].length(); k < n && addGap(gaps, getGap(tmp[j].charAt(k), ctx.localGapSpec)); ++k) ; } } if (!gaps.isEmpty()) { StringBuilder sb = new StringBuilder(); flushGaps(gaps, sb, false); rh = sb.substring(0, sb.length() - 1); } } if (rh == null) { if (gapRow && lastGapRowHeight == null) { ctx.b = new DefaultFormBuilder(new FormLayout(colstmp.toString(), "")); Prop.setBuilder(props, ctx); lastGapRowHeight = parseableRowSpec(ctx.b.getLineGapSpec()); } rh = gapRow ? lastGapRowHeight : lastContentRowHeight; } else { if (gapRow) lastGapRowHeight = rh; else lastContentRowHeight = rh; } if (i > 0) rowstmp.append(','); rowstmp.append(rh); } // Create builder // FormLayout fml = new FormLayout(colstmp.toString(), rowstmp.toString()); ctx.b = new DefaultFormBuilder(fml, debuggable()); // Apply builder settings (e.g. column groups) // Prop.setBuilder(props, ctx); props = null; // Set row groups // if (!rowGroups.isEmpty()) { int[][] tmp = new int[rowGroups.size()][]; // ??? i = 0; for (int[] a : rowGroups.values()) tmp[i++] = a; fml.setRowGroups(tmp); } rowGroups = null; JLabel lastLabel = null; java.util.HashSet<Character> done = new java.util.HashSet<Character>(ctx.components.size()); int h = cells.length; for (int y = 0; y < cells.length; ++y) { int w = cells[y].length(); int first = -1; for (int x = 0; x < w; ++x) { char ch = cells[y].charAt(x); if (ch == '_' || done.contains(ch)) continue; int idx = intValue(ch); Rec rec; try { rec = ctx.components.get(idx); } catch (IndexOutOfBoundsException e) { throw new IndexOutOfBoundsException( String.format("build() cells=\"%s\" ch=%c rows=\"%s\"", cells[y], ch, rows)); } CellConstraints cc = (rec.cc == null) ? new CellConstraints() : (CellConstraints) rec.cc.clone(); int sx = cc.gridWidth, sy = cc.gridHeight; // span x, span y while (x + sx < w && cells[y].charAt(x + sx) == ch) sx += 1; while (y + sy < h && ((x < cells[y + sy].length() && cells[y + sy].charAt(x) == ch) || (cells[y + sy].length() == 0 && y + sy + 1 < h && x < cells[y + sy + 1].length() && cells[y + sy + 1].charAt(x) == ch))) { sy += 1; } int colSpan = ctx.contentCol[x + sx - 1] - ctx.contentCol[x] + 1; ctx.b.setBounds(ctx.contentCol[x] + 1, ctx.b.getRow(), colSpan, sy); ctx.b.setLeadingColumnOffset(first & ctx.contentCol[x]); // 0 vagy x (itt nem kell a +1) first = 0; x += (sx - 1); Object comp = ctx.components.get(idx).component; if (comp instanceof Component) { ctx.b.append((Component) comp, colSpan); if (comp instanceof JLabel) lastLabel = (JLabel) comp; else { if (lastLabel != null) lastLabel.setLabelFor((Component) comp); lastLabel = null; } } else if (comp instanceof Separator) { comp = ctx.b.appendSeparator(comp.toString()); lastLabel = null; } else { comp = lastLabel = ctx.b.getComponentFactory().createLabel(comp.toString()); ctx.b.append(lastLabel, colSpan); } if (rec.useAlignment || rec.useInsets) { CellConstraints cc2 = fml.getConstraints((Component) comp); cc2.insets = cc.insets; cc2.hAlign = cc.hAlign; cc2.vAlign = cc.vAlign; fml.setConstraints((Component) comp, cc2); } done.add(ch); } lastLabel = null; ctx.b.nextLine(); } return ctx.b; } //------------------------------------------------------------------------- public static class Separator { String title; public Separator(String title) { this.title = title; } @Override public String toString() { return title; } } //------------------------------------------------------------------------- public static class CellInsets { CellConstraints cc; boolean useAlignment = false; public CellInsets(int px_top, int px_left, int px_bottom, int px_right) { this(px_top, px_left, px_bottom, px_right, (String) null); } /** @param c used for dlu comptation */ public CellInsets(int dlu_top, int dlu_left, int dlu_bottom, int dlu_right, Component c) { this(dlu_top, dlu_left, dlu_bottom, dlu_right, (String) null, c); } public CellInsets(int px_top, int px_left, int px_bottom, int px_right, String alignments) { useAlignment = (alignments != null); cc = (useAlignment) ? new CellConstraints("1,1," + alignments) : new CellConstraints(); cc.insets = new java.awt.Insets(px_top, px_left, px_bottom, px_right); } /** @param c used for dlu comptation */ public CellInsets(int dlu_top, int dlu_left, int dlu_bottom, int dlu_right, String alignments, Component c) { this(Sizes.dialogUnitYAsPixel(dlu_top, c), Sizes.dialogUnitXAsPixel(dlu_left, c), Sizes.dialogUnitYAsPixel(dlu_bottom, c), Sizes.dialogUnitXAsPixel(dlu_right, c), alignments); } } //------------------------------------------------------------------------- public static <B extends PanelBuilder> B titledPanel(String title, B pb) { titledBorder(title, pb.getPanel()); return pb; } //------------------------------------------------------------------------- public static <B extends javax.swing.JComponent> B titledBorder(String title, B comp) { comp.setBorder(BorderFactory.createTitledBorder(title)); return comp; } //------------------------------------------------------------------------- public static <B extends javax.swing.JComponent> B emptyBorder(String encodedSizes, B comp) { comp.setBorder(Borders.createEmptyBorder(decodeGapChars(encodedSizes, true, null))); return comp; } //------------------------------------------------------------------------- public static JPanel debuggable() { return (isDebug) ? new FormDebugPanel() : new JPanel(); } //------------------------------------------------------------------------- public static String decodeGapChars(String s, boolean horizontal, java.util.Map<Character, String> localGapSpec) { StringBuilder sb = new StringBuilder(); parseColumnWidths(sb, s, localGapSpec, (horizontal ? 0 : 1) | 2 | 4); return sb.toString(); } //------------------------------------------------------------------------- static class Rec { Object component; CellConstraints cc = null; boolean useAlignment = false; boolean useInsets = false; Rec(Object comp) { component = comp; } } //------------------------------------------------------------------------- public static enum Prop { /** Example: "[LineGapSize=3dlu]" <br>It must be a constant value */ LineGapSize { @Override Object parseGeneralSetting(Context c) { return Sizes.constant(c.input, false); } @Override void setBuilder(Object value, Context c) { c.b.setLineGapSize((ConstantSize) value); } }, /** Example: "[ParagraphGapSize=6dlu]" <br>It must be a constant value */ ParagraphGapSize { @Override Object parseGeneralSetting(Context c) { return Sizes.constant(c.input, false); } @Override void setBuilder(Object value, Context c) { c.b.setParagraphGapSize((ConstantSize) value); } }, /** Example: "[DialogBorder]" */ DialogBorder { @Override void setBuilder(Object value, Context c) { c.b.setDefaultDialogBorder(); } }, /** Example: "[EmptyBorder=0~0~]". Top left bottom right sizes spearated by spaces. */ EmptyBorder { @Override Object parseGeneralSetting(Context c) { return decodeGapChars(c.input, true, c.localGapSpec); } @Override void setBuilder(Object value, Context c) { emptyBorder(value.toString(), c.b.getPanel()); } }, /** Example: "[ColGroups=01-23]". Gap-columns must be counted. */ ColGroups { @Override Object parseGeneralSetting(Context c) { StringTokenizer t = new StringTokenizer(c.input, "-"); int n = t.countTokens(), colGroups[][] = new int[n][]; for (int i = 0; i < n; ++i) { String s = t.nextToken(); int sl = s.length(); colGroups[i] = new int[sl]; while (--sl >= 0) colGroups[i][sl] = intValue(s.charAt(sl)); } return colGroups; } @Override void setBuilder(Object value, Context c) { c.b.getLayout().setColumnGroups((int[][]) value); } }; // Default implementation Object parseGeneralSetting(Context c) { return null; } void setBuilder(Object value, Context c) { } // The string to be parsed is expected in 'c.input'. This string may be replaced. static EnumMap<Prop, Object> parseGeneralSettings(Context c) { EnumMap<Prop, Object> ans = new EnumMap<Prop, Object>(Prop.class); String str = c.input; W: while (str != null && str.length() > 0) { for (Prop p : values()) { if (str.startsWith(p.name())) { int i = str.indexOf(','), j = p.name().length(); if (i < 0) i = str.length(); if (j < str.length() && str.charAt(j) == '=') j += 1; c.input = str.substring(j, i); ans.put(p, p.parseGeneralSetting(c)); str = str.substring(i).replaceFirst("[,\\s]*", ""); continue W; } } break; } return ans; } static void setBuilder(EnumMap<Prop, Object> settings, Context c) { if (settings != null) for (Entry<Prop, Object> e : settings.entrySet()) e.getKey().setBuilder(e.getValue(), c); } }; //------------------------------------------------------------------------- static class Context { String input = ""; java.util.Map<Character, String> localGapSpec = null; ArrayList<Rec> components = null; int[] contentCol = null; DefaultFormBuilder b = null; }; //------------------------------------------------------------------------- /** Converts a RowSpec object to parseable string format */ public static String parseableRowSpec(RowSpec rowspec) { return parseableSize(rowspec.toString()).replace(":noGrow", ""); } //------------------------------------------------------------------------- /** Converts a Size object (e.g. ConstantSize) to parseable string format */ public static String parseableSize(String sizeStr) { return sizeStr.replaceAll("dlu[XY]", "dlu"); } //------------------------------------------------------------------------- public static int[] appendIntArray(int[] array, int value) { if (array == null || array.length == 0) return new int[] { value }; int[] ans = new int[array.length + 1]; System.arraycopy(array, 0, ans, 0, array.length); ans[array.length] = value; return ans; } //------------------------------------------------------------------------- public static int intValue(char ch) { int ans = Integer.parseInt(String.valueOf(ch), 36); if (ch > 'Z') ans += 26; // lowercase letters mean higher values return ans; } //------------------------------------------------------------------------- public static String getGap(Character ch, java.util.Map<Character, String> local) { String ans = (local != null) ? local.get(ch) : null; if (ans == null) ans = gapLength.get(ch); return ans; } //------------------------------------------------------------------------- private static int[] parseColumnWidths(StringBuilder colstmp, String cols, java.util.Map<Character, String> localGapSpec, int flags) { boolean horizontal = (flags & 1) == 0; boolean allowAdjacentGapCols = (flags & 2) != 0; boolean returnContentCols = (flags & 4) == 0; java.util.Set<Character> gapChars = gapLength.keySet(); if (localGapSpec != null) { gapChars = new java.util.HashSet<Character>(gapChars); gapChars.addAll(localGapSpec.keySet()); } StringBuilder gapcharstr = new StringBuilder(gapChars.size()); for (Character ch : gapChars) gapcharstr.append(ch); gapcharstr.append(", "); colstmp.setLength(0); int contentCol[] = null; // contentCol[content_column] == FormLayout_column int nCols = 0; // number of FormLayout-columns (includes gap-columns) StringTokenizer t = new StringTokenizer(cols, gapcharstr.toString(), true); ArrayList<String> gaps = new ArrayList<String>(); while (t.hasMoreTokens()) { String s = t.nextToken(); if (s.length() == 1) { char ch = s.charAt(0); if (ch == ' ' || ch == ',') { if (!allowAdjacentGapCols) continue; } else if (addGap(gaps, getGap(ch, localGapSpec))) continue; } nCols += flushGaps(gaps, colstmp, true); if (s != null) { if (returnContentCols) contentCol = appendIntArray(contentCol, nCols++); colstmp.append(s); colstmp.append(','); } } nCols += flushGaps(gaps, colstmp, horizontal); int l = colstmp.length(); if (l > 0 && colstmp.charAt(l - 1) == ',') colstmp.setLength(l - 1); return contentCol; } //------------------------------------------------------------------------- private static boolean addGap(ArrayList<String> gaps, String gap) { if (gap == null) return false; gaps.add(gap); return true; } //------------------------------------------------------------------------- private static int flushGaps(ArrayList<String> gaps, StringBuilder colstmp, boolean horizontal) { if (gaps.isEmpty()) return 0; if (gaps.size() == 1) { colstmp.append(gaps.get(0)); colstmp.append(','); } else { int pixelSize = 0; for (String size : gaps) { // Ide a 'null' helyere erdemes lenne bejuttatni egy GUI komponenst, ha lehet pixelSize += Sizes.constant(size, horizontal).getPixelSize(null); } colstmp.append(pixelSize); colstmp.append("px,"); } gaps.clear(); return 1; } }