All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.privatejgoodies.forms.layout.LayoutMap Maven / Gradle / Ivy

/*
 * Copyright (c) 2002-2013 JGoodies Software GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of JGoodies Software GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.privatejgoodies.forms.layout;

import static com.privatejgoodies.common.base.Preconditions.checkNotNull;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;

import com.privatejgoodies.forms.util.LayoutStyle;

/**
 * Provides a hierarchical variable expansion useful to improve layout consistency, style guide
 * compliance, and layout readability.

* * A LayoutMap maps variable names to layout expression Strings. The FormLayout, ColumnSpec, and * RowSpec parsers expand variables before an encoded layout specification is parsed and converted * into ColumnSpec and RowSpec values. Variables start with the '$' character. The variable name can * be wrapped by braces ('{' and '}'). For example, you can write: * new FormLayout("pref, $lcg, pref") or * new FormLayout("pref, ${lcg}, pref").

* * LayoutMaps build a chain; each LayoutMap has an optional parent map. The root is defined by * {@link LayoutMap#getRoot()}. Application-wide variables should be defined in the root LayoutMap. * If you want to override application-wide variables locally, obtain a LayoutMap using {@code * new LayoutMap()}, configure it, and provide it as argument to the FormLayout, ColumnSpec, and * RowSpec constructors/factory methods.

* * By default the root LayoutMap provides the following associations: *

* * * * * * * * *
Variable * NameAbbreviationsOrientationDescription
label-component-gaplcg, lcgapbothgap between a label and the * labeled component
related-gaprg, rgapbothgap between two related * components
unrelated-gapug, ugapbothgap between two unrelated * components
buttonbhorizontalbutton column with minimum width
line-gaplg, lgapverticalgap between two lines
narrow-line-gapnlg, nlgapverticalnarrow gap between two * lines
paragraphpg, pgapverticalgap between two * paragraphs/sections

* * Examples: *

 * // Predefined variables
 * new FormLayout(
 *     "pref, $lcgap, pref, $rgap, pref",
 *     "p, $lgap, p, $lgap, p");
 *
 * // Custom variables
 * LayoutMap.getRoot().columnPut("half", "39dlu");
 * LayoutMap.getRoot().columnPut("full", "80dlu");
 * LayoutMap.getRoot().rowPut("table", "fill:0:grow");
 * LayoutMap.getRoot().rowPut("table50", "fill:50dlu:grow");
 * new FormLayout(
 *     "pref, $lcgap, $half, 2dlu, $half",
 *     "p, $lcgap, $table50");
 * new FormLayout(
 *     "pref, $lcgap, $full",
 *     "p, $lcgap, $table50");
 *
 * // Nested variables
 * LayoutMap.getRoot().columnPut("c-gap-c", "$half, 2dlu, $half");
 * new FormLayout(
 *     "pref, $lcgap, ${c-gap-c}", // -> "pref, $lcgap, $half, 2dlu, $half",
 *     "p, $lcgap, $table");
 * 
* * LayoutMap holds two internal Maps that associate key Strings with expression Strings for the * columns and rows respectively. Null values are not allowed.

* * Tips:

    *
  • You should carefully override predefined variables, because variable users may expect that * these don't change. *
  • Set custom variables in the root LayoutMap. *
  • Avoid aliases for custom variables. *
* * @author Karsten Lentzsch * @version $Revision: 1.24 $ * * @see FormLayout * @see ColumnSpec * @see RowSpec * * @since 1.2 */ public final class LayoutMap { /** * Marks a layout variable; used by the Forms parsers. */ private static final char VARIABLE_PREFIX_CHAR = '$'; /** * Maps column aliases to their default name, for example * {@code "rgap"} -> {@code "related-gap"}. */ private static final Map COLUMN_ALIASES = new HashMap(); /** * Maps row aliases to their default name, for example {@code "rgap"} -> {@code "related-gap"}. */ private static final Map ROW_ALIASES = new HashMap(); /** * Holds the lazily initialized root map. */ private static LayoutMap root = null; // Instance Fields ******************************************************** /** * Refers to the parent map that is used to look up values if this map contains no association * for a given key. The parent maps can build chains. */ private final LayoutMap parent; /** * Holds the raw associations from variable names to expressions. The expression may contain * variables that are not expanded. */ private final Map columnMap; /** * Holds the cached associations from variable names to expressions. The expression are fully * expanded and contain no variables. */ private final Map columnMapCache; /** * Holds the raw associations from variable names to expressions. The expression may contain * variables that are not expanded. */ private final Map rowMap; /** * Holds the cached associations from variable names to expressions. The expression are fully * expanded and contain no variables. */ private final Map rowMapCache; // Instance Creation ****************************************************** /** * Constructs a LayoutMap that has the root LayoutMap as parent. */ public LayoutMap() { this(getRoot()); } /** * Constructs a LayoutMap with the given optional parent. * * @param parent the parent LayoutMap, may be {@code null} */ public LayoutMap(LayoutMap parent) { this.parent = parent; columnMap = new HashMap(); rowMap = new HashMap(); columnMapCache = new HashMap(); rowMapCache = new HashMap(); } // Default **************************************************************** /** * Lazily initializes and returns the LayoutMap that is used for variable expansion, if no * custom LayoutMap is provided. * * @return the LayoutMap that is used, if no custom LayoutMap is provided */ public static synchronized LayoutMap getRoot() { if (root == null) { root = createRoot(); } return root; } // Column Mapping ********************************************************* /** * Returns {@code true} if this map or a parent map - if any - contains a mapping for the * specified key. * * @param key key whose presence in this LayoutMap chain is to be tested. * @return {@code true} if this map contains a column mapping for the specified key. * * @throws NullPointerException if the key is {@code null}. * * @see Map#containsKey(Object) */ public boolean columnContainsKey(String key) { String resolvedKey = resolveColumnKey(key); return columnMap.containsKey(resolvedKey) || parent != null && parent.columnContainsKey(resolvedKey); } /** * Looks up and returns the String associated with the given key. First looks for an association * in this LayoutMap. If there's no association, the lookup continues with the parent map - if * any. * * @param key key whose associated value is to be returned. * @return the column String associated with the {@code key}, or {@code null} if no LayoutMap in * the parent chain contains an association. * * @throws NullPointerException if {@code key} is {@code null} * * @see Map#get(Object) */ public String columnGet(String key) { String resolvedKey = resolveColumnKey(key); String cachedValue = columnMapCache.get(resolvedKey); if (cachedValue != null) { return cachedValue; } String value = columnMap.get(resolvedKey); if (value == null && parent != null) { value = parent.columnGet(resolvedKey); } if (value == null) { return null; } String expandedString = expand(value, true); columnMapCache.put(resolvedKey, expandedString); return expandedString; } /** * Associates the specified column String with the specified key in this map. If the map * previously contained a mapping for this key, the old value is replaced by the specified * value. The value set in this map overrides an association - if any - in the chain of parent * LayoutMaps.

* * The {@code value} must not be {@code null}. To remove an association from this map use * {@link #columnRemove(String)}. * * @param key key with which the specified value is to be associated. * @param value column expression value to be associated with the specified key. * @return previous String associated with specified key, or {@code null} if there was no * mapping for key. * * @throws NullPointerException if the {@code key} or {@code value} is {@code null}. * * @see Map#put(Object, Object) */ public String columnPut(String key, String value) { checkNotNull(value, "The column expression value must not be null."); String resolvedKey = resolveColumnKey(key); columnMapCache.clear(); return columnMap.put( resolvedKey, value.toLowerCase(Locale.ENGLISH)); } public String columnPut(String key, ColumnSpec value) { return columnPut(key, value.encode()); } public String columnPut(String key, Size value) { return columnPut(key, value.encode()); } /** * Removes the column value mapping for this key from this map if it is present.

* * Returns the value to which the map previously associated the key, or {@code null} if the map * contained no mapping for this key. The map will not contain a String mapping for the * specified key once the call returns. * * @param key key whose mapping is to be removed from the map. * @return previous value associated with specified key, or {@code null} if there was no mapping * for key. * * @throws NullPointerException if {@code key} is {@code null}. * * @see Map#remove(Object) */ public String columnRemove(String key) { String resolvedKey = resolveColumnKey(key); columnMapCache.clear(); return columnMap.remove(resolvedKey); } // Row Mapping ************************************************************ /** * Returns {@code true} if this map or a parent map - if any - contains a RowSpec mapping for * the specified key. * * @param key key whose presence in this LayoutMap chain is to be tested. * @return {@code true} if this map contains a row mapping for the specified key. * * @throws NullPointerException if the key is {@code null}. * * @see Map#containsKey(Object) */ public boolean rowContainsKey(String key) { String resolvedKey = resolveRowKey(key); return rowMap.containsKey(resolvedKey) || parent != null && parent.rowContainsKey(resolvedKey); } /** * Looks up and returns the RowSpec associated with the given key. First looks for an * association in this LayoutMap. If there's no association, the lookup continues with the * parent map - if any. * * @param key key whose associated value is to be returned. * @return the row specification associated with the {@code key}, or {@code null} if no * LayoutMap in the parent chain contains an association. * * @throws NullPointerException if {@code key} is {@code null} * * @see Map#get(Object) */ public String rowGet(String key) { String resolvedKey = resolveRowKey(key); String cachedValue = rowMapCache.get(resolvedKey); if (cachedValue != null) { return cachedValue; } String value = rowMap.get(resolvedKey); if (value == null && parent != null) { value = parent.rowGet(resolvedKey); } if (value == null) { return null; } String expandedString = expand(value, false); rowMapCache.put(resolvedKey, expandedString); return expandedString; } public String rowPut(String key, String value) { checkNotNull(value, "The row expression value must not be null."); String resolvedKey = resolveRowKey(key); rowMapCache.clear(); return rowMap.put( resolvedKey, value.toLowerCase(Locale.ENGLISH)); } /** * Associates the specified ColumnSpec with the specified key in this map. If the map previously * contained a mapping for this key, the old value is replaced by the specified value. The * RowSpec set in this map override an association - if any - in the chain of parent * LayoutMaps.

* * The RowSpec must not be {@code null}. To remove an association from this map use * {@link #rowRemove(String)}. * * @param key key with which the specified value is to be associated. * @param value ColumnSpec to be associated with the specified key. * @return previous ColumnSpec associated with specified key, or {@code null} if there was no * mapping for key. * * @throws NullPointerException if the {@code key} or {@code value} is {@code null}. * * @see Map#put(Object, Object) */ public String rowPut(String key, RowSpec value) { return rowPut(key, value.encode()); } public String rowPut(String key, Size value) { return rowPut(key, value.encode()); } /** * Removes the row value mapping for this key from this map if it is present.

* * Returns the value to which the map previously associated the key, or {@code null} if the map * contained no mapping for this key. The map will not contain a String mapping for the * specified key once the call returns. * * @param key key whose mapping is to be removed from the map. * @return previous value associated with specified key, or {@code null} if there was no mapping * for key. * * @throws NullPointerException if {@code key} is {@code null}. * * @see Map#remove(Object) */ public String rowRemove(String key) { String resolvedKey = resolveRowKey(key); rowMapCache.clear(); return rowMap.remove(resolvedKey); } // Overriding Object Behavior ********************************************* /** * Returns a string representation of this LayoutMap that lists the column and row associations. * * @return a string representation */ @Override public String toString() { StringBuffer buffer = new StringBuffer(super.toString()); buffer.append("\n Column associations:"); for (Entry entry : columnMap.entrySet()) { buffer.append("\n "); buffer.append(entry.getKey()); buffer.append("->"); buffer.append(entry.getValue()); } buffer.append("\n Row associations:"); for (Entry entry : rowMap.entrySet()) { buffer.append("\n "); buffer.append(entry.getKey()); buffer.append("->"); buffer.append(entry.getValue()); } return buffer.toString(); } // String Expansion ******************************************************* String expand(String expression, boolean horizontal) { int cursor = 0; int start = expression.indexOf(LayoutMap.VARIABLE_PREFIX_CHAR, cursor); if (start == -1) { // No variables return expression; } StringBuffer buffer = new StringBuffer(); do { buffer.append(expression.substring(cursor, start)); String variableName = nextVariableName(expression, start); buffer.append(expansion(variableName, horizontal)); cursor = start + variableName.length() + 1; start = expression.indexOf(LayoutMap.VARIABLE_PREFIX_CHAR, cursor); } while (start != -1); buffer.append(expression.substring(cursor)); return buffer.toString(); } private static String nextVariableName(String expression, int start) { int length = expression.length(); if (length <= start) { FormSpecParser.fail(expression, start, "Missing variable name after variable char '$'."); } if (expression.charAt(start + 1) == '{') { int end = expression.indexOf('}', start + 1); if (end == -1) { FormSpecParser.fail(expression, start, "Missing closing brace '}' for variable."); } return expression.substring(start + 1, end + 1); } int end = start + 1; while (end < length && Character.isUnicodeIdentifierPart(expression.charAt(end))) { end++; } return expression.substring(start + 1, end); } private String expansion(String variableName, boolean horizontal) { String key = stripBraces(variableName); String expansion = horizontal ? columnGet(key) : rowGet(key); if (expansion == null) { String orientation = horizontal ? "column" : "row"; throw new IllegalArgumentException("Unknown " + orientation + " layout variable \"" + key + "\""); } return expansion; } private static String stripBraces(String variableName) { return variableName.charAt(0) == '{' ? variableName.substring(1, variableName.length() - 1) : variableName; } // Helper Code ************************************************************ private static String resolveColumnKey(String key) { checkNotNull(key, "The column key must not be null."); String lowercaseKey = key.toLowerCase(Locale.ENGLISH); String defaultKey = COLUMN_ALIASES.get(lowercaseKey); return defaultKey == null ? lowercaseKey : defaultKey; } private static String resolveRowKey(String key) { checkNotNull(key, "The row key must not be null."); String lowercaseKey = key.toLowerCase(Locale.ENGLISH); String defaultKey = ROW_ALIASES.get(lowercaseKey); return defaultKey == null ? lowercaseKey : defaultKey; } private static LayoutMap createRoot() { LayoutMap map = new LayoutMap(null); // Column variables map.columnPut( "label-component-gap", new String[]{"lcg", "lcgap"}, FormSpecs.LABEL_COMPONENT_GAP_COLSPEC); map.columnPut( "related-gap", new String[]{"rg", "rgap"}, FormSpecs.RELATED_GAP_COLSPEC); map.columnPut( "unrelated-gap", new String[]{"ug", "ugap"}, FormSpecs.UNRELATED_GAP_COLSPEC); map.columnPut( "button", new String[]{"b"}, FormSpecs.BUTTON_COLSPEC); map.columnPut( "growing-button", new String[]{"gb"}, FormSpecs.GROWING_BUTTON_COLSPEC); map.columnPut( "dialog-margin", new String[]{"dm", "dmargin"}, ColumnSpec.createGap(LayoutStyle.getCurrent().getDialogMarginX())); map.columnPut( "tabbed-dialog-margin", new String[]{"tdm", "tdmargin"}, ColumnSpec.createGap(LayoutStyle.getCurrent().getTabbedDialogMarginX())); map.columnPut( "glue", FormSpecs.GLUE_COLSPEC.toShortString()); // Row variables map.rowPut( "label-component-gap", new String[]{"lcg", "lcgap"}, FormSpecs.LABEL_COMPONENT_GAP_ROWSPEC); map.rowPut( "related-gap", new String[]{"rg", "rgap"}, FormSpecs.RELATED_GAP_ROWSPEC); map.rowPut( "unrelated-gap", new String[]{"ug", "ugap"}, FormSpecs.UNRELATED_GAP_ROWSPEC); map.rowPut( "narrow-line-gap", new String[]{"nlg", "nlgap"}, FormSpecs.NARROW_LINE_GAP_ROWSPEC); map.rowPut( "line-gap", new String[]{"lg", "lgap"}, FormSpecs.LINE_GAP_ROWSPEC); map.rowPut( "paragraph-gap", new String[]{"pg", "pgap"}, FormSpecs.PARAGRAPH_GAP_ROWSPEC); map.rowPut( "dialog-margin", new String[]{"dm", "dmargin"}, RowSpec.createGap(LayoutStyle.getCurrent().getDialogMarginY())); map.rowPut( "tabbed-dialog-margin", new String[]{"tdm", "tdmargin"}, RowSpec.createGap(LayoutStyle.getCurrent().getTabbedDialogMarginY())); map.rowPut( "button", new String[]{"b"}, FormSpecs.BUTTON_ROWSPEC); map.rowPut( "glue", FormSpecs.GLUE_ROWSPEC); return map; } private void columnPut(String key, String[] aliases, ColumnSpec value) { ensureLowerCase(key); columnPut(key, value); for (String aliase : aliases) { ensureLowerCase(aliase); COLUMN_ALIASES.put(aliase, key); } } private void rowPut(String key, String[] aliases, RowSpec value) { ensureLowerCase(key); rowPut(key, value); for (String aliase : aliases) { ensureLowerCase(aliase); ROW_ALIASES.put(aliase, key); } } private static void ensureLowerCase(String str) { String lowerCase = str.toLowerCase(Locale.ENGLISH); if (!lowerCase.equals(str)) { throw new IllegalArgumentException( "The string \"" + str + "\" should be lower case."); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy