/* * Copyright (c) 2004-2007 by Michael Connor. 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 FormLayoutBuilder or Michael Connor 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 org.mlc.swing.layout; import java.awt.Component; import java.awt.Container; import java.awt.Insets; import java.beans.Statement; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JRadioButton; import javax.swing.JToggleButton; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import com.jgoodies.forms.layout.CellConstraints; /** * This class handles the serialization and deserialization of the xml files * that we are using to store the layout constraints. * * <p>In the consuming program, the use of this class might look like: <br><code> InputStream constraints = this.getClass().getResourceAsStream(xmlFile); LayoutConstraintsManager layoutConstraintsManager = LayoutConstraintsManager.getLayoutConstraintsManager(constraints); LayoutManager layout = layoutConstraintsManager.createLayout("panel", this); this.setLayout(layout); </code> * * <p>[I'm sure there are more * elegant ways of handling this (like JAXB) or some other mapping software but * this is simple, it works, and we don't have to package a bunch of other * software or files.] * * @author Michael Connor @version $Id$ @since Ptolemy II 7.1 */ public class LayoutConstraintsManager { String defaultColumnSpecs = "right:max(30dlu;pref),3dlu,80dlu,10dlu,right:max(30dlu;pref),3dlu,80dlu,1dlu:grow"; String defaultRowSpecs = "pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref:grow"; static Set<Class> textComponents = new HashSet<Class>(); static { textComponents.add(JButton.class); textComponents.add(JCheckBox.class); textComponents.add(JRadioButton.class); textComponents.add(JToggleButton.class); textComponents.add(JLabel.class); } Map<ContainerLayout, Container> containers = new HashMap<ContainerLayout, Container>(); List<ContainerLayout> layouts = new ArrayList<ContainerLayout>(); /** * This method will create a LayoutConstraintsManager with default JGoodies * row and column specs that are common in applications. The user can then * manipulate these default specs using the LayoutFrame to fine tune the specs * to be whatever they want. */ public LayoutConstraintsManager() { } /** * This method will create a LayoutConstraintsManager with the JGoodies specs * provided as default */ public LayoutConstraintsManager(String defaultColumnSpecs, String defaultRowSpecs) { this.defaultColumnSpecs = defaultColumnSpecs; this.defaultRowSpecs = defaultRowSpecs; } public List<ContainerLayout> getLayouts() { List<ContainerLayout> list = new ArrayList<ContainerLayout>(layouts .size()); list.addAll(layouts); return list; } /** * This method will build a layout from the xml file based on the name and * call setLayout on the container passed in. */ public void setLayout(String name, Container container) { ContainerLayout containerLayout = getLayout(name); if (containerLayout == null) { containerLayout = new ContainerLayout(name, defaultColumnSpecs, defaultRowSpecs); layouts.add(containerLayout); } // else // containers.remove(containerLayout); container.setLayout(containerLayout); containers.put(containerLayout, container); } /** * This method creates a layout by first trying to look in memory to see if a * layout has been defined with the given name. If a layout exists, it is * returned. Note that when I say in memory that probably means that it was * defined in the xml file. If one doesn't exist, a layout with what I * consider relatively generic constraints will be created and returned. The * reason this method requires the container is because in the case where you * are trying to layout the container visually, I need to be able to get a * handle on the container so I can make calls to add components to it during * interactive layout. This will not be touched at runtime if you are not * doing anything interactively. This method should be seen as a replacement * for LayoutConstraintsManager.setLayout(String name, Container container) as * it's more natural to set the layout on the container yourself. */ public ContainerLayout createLayout(String name, Container container) { ContainerLayout containerLayout = getLayout(name); if (containerLayout == null) { containerLayout = new ContainerLayout(name, defaultColumnSpecs, defaultRowSpecs); layouts.add(containerLayout); } containers.put(containerLayout, container); return containerLayout; } public Container getContainer(ContainerLayout layout) { return containers.get(layout); } private ContainerLayout getLayout(String name) { for (int i = 0; i < layouts.size(); i++) { if (layouts.get(i).getName().equals(name)) { return layouts.get(i); } } return null; } // KBR NYI // public List<String> getContainerNames() // { // List<String> names = new ArrayList<String>(); // // return names; // } public ContainerLayout getContainerLayout(String containerName) { ContainerLayout layout = getLayout(containerName); return layout; } public void removeLayout(ContainerLayout containerLayout) { layouts.remove(containerLayout); } public void addLayout(ContainerLayout containerLayout) { layouts.add(containerLayout); } /** Get an XML representation of the FormLayout constraints for all containers in this manager. */ public String getXML() { StringBuffer xml = new StringBuffer( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n"); xml.append("<containers>\n"); for (int index = 0; index < layouts.size(); index++) { ContainerLayout layout = layouts.get(index); LinkedHashMap<String, CellConstraints> constraintMap = layout .getCellConstraints(); xml.append(" <container name=\"" + layout.getName() + "\"\n"); xml.append(" columnSpecs=\"" + layout.getColumnSpecsString() + "\"\n"); xml.append(" rowSpecs=\"" + layout.getRowSpecsString() + "\">\n"); for (Iterator componentNames = constraintMap.keySet().iterator(); componentNames .hasNext();) { String componentName = (String) componentNames.next(); CellConstraints constraints = constraintMap.get(componentName); xml.append(" <cellconstraints "); xml.append("name=\"" + componentName + "\" "); xml.append("gridX=\"" + constraints.gridX + "\" "); xml.append("gridY=\"" + constraints.gridY + "\" "); xml.append("gridWidth=\"" + constraints.gridWidth + "\" "); xml.append("gridHeight=\"" + constraints.gridHeight + "\" "); xml.append("horizontalAlignment=\"" + getAlignment(constraints.hAlign) + "\" "); xml.append("verticalAlignment=\"" + getAlignment(constraints.vAlign) + "\" "); xml.append("topInset=\"" + constraints.insets.top + "\" "); xml .append("bottomInset=\"" + constraints.insets.bottom + "\" "); xml.append("rightInset=\"" + constraints.insets.right + "\" "); xml.append("leftInset=\"" + constraints.insets.left + "\"/>\n"); } for (Iterator componentNames = constraintMap.keySet().iterator(); componentNames .hasNext();) { String componentName = (String) componentNames.next(); Component component = layout.getComponentByName(componentName); if (component != null) { Map<String, Object> customProperties = layout .getCustomProperties(componentName); boolean hasProperties = false; boolean isTextComponent = isTextComponent(component); // we need to look through these and see if we have // any valid properties. the text props don't count // for controls like JLabel, JButton, etc. because // we'll put those in the constructor. // EAL: In Ptolemy, we need text in the properties // fields. So remove this check below, and force // isTextComponent to false. isTextComponent = false; //for (String propertyName : customProperties.keySet()) { // if ((!isTextComponent) /* EAL: || (!propertyName.equals("text")) */) { // hasProperties = true; // break; // } //} if (!customProperties.keySet().isEmpty()) { hasProperties = true; } if (hasProperties) { xml.append("\n <properties component=\"" + componentName + "\">\n"); for (String propertyName : customProperties.keySet()) { if (isTextComponent && propertyName.equals("text")) { break; } ByteArrayOutputStream stream = new ByteArrayOutputStream(); XMLEncoder xmlEncoder = new XMLEncoder(stream); xmlEncoder.setOwner(component); String methodName = "set" + propertyName.substring(0, 1) .toUpperCase() + (propertyName.length() > 1 ? propertyName .substring(1) : ""); xmlEncoder.writeStatement(new Statement(xmlEncoder, methodName, new Object[] { customProperties .get(propertyName) })); xmlEncoder.close(); String propertyXml = new String(stream .toByteArray()); int voidStart = propertyXml.indexOf("<void"); int voidEnd = propertyXml.indexOf(">", voidStart); int end = propertyXml.lastIndexOf("</void>"); String xmlWithoutDec = propertyXml.substring( voidEnd + 1, end); xmlWithoutDec = xmlWithoutDec.trim(); String indented = " " + xmlWithoutDec.replaceAll("\n", "\n "); xml.append(" <property name=\"" + propertyName + "\">"); xml.append(indented); xml.append("</property>\n"); } xml.append(" </properties>\n"); } } } xml.append(" </container>\n"); } xml.append("</containers>\n"); return xml.toString(); } public static boolean isTextComponent(Component component) { for (Class clazz : textComponents) { if (clazz.isAssignableFrom(component.getClass())) { return true; } } return false; } private static String createString(NodeList childNodes) { try { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); TransformerFactory tFactory = TransformerFactory.newInstance(); Transformer transformer = tFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); DOMSource source = new DOMSource(child); StreamResult result = new StreamResult(byteStream); transformer.transform(source, result); } return new String(byteStream.toByteArray()); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("unexpected error"); } } public static final String DEFAULT = "default"; public static final String FILL = "fill"; public static final String CENTER = "center"; public static final String LEFT = "left"; public static final String RIGHT = "right"; public static final String TOP = "top"; public static final String BOTTOM = "bottom"; /** * Translates an alignment value to a string. */ public static String getAlignment(CellConstraints.Alignment alignment) { String value = null; if (alignment == CellConstraints.DEFAULT) { value = DEFAULT; } else if (alignment == CellConstraints.FILL) { value = FILL; } else if (alignment == CellConstraints.CENTER) { value = CENTER; } else if (alignment == CellConstraints.LEFT) { value = LEFT; } else if (alignment == CellConstraints.RIGHT) { value = RIGHT; } else if (alignment == CellConstraints.TOP) { value = TOP; } else if (alignment == CellConstraints.BOTTOM) { value = BOTTOM; } if (value == null) { throw new RuntimeException("Unknown alignment type"); } else { return value; } } /** * Translates a string to an alignment value. */ public static CellConstraints.Alignment getAlignment(String value) { CellConstraints.Alignment alignment = null; if (value.equalsIgnoreCase(DEFAULT)) { alignment = CellConstraints.DEFAULT; } else if (value.equalsIgnoreCase(FILL)) { alignment = CellConstraints.FILL; } else if (value.equalsIgnoreCase(CENTER)) { alignment = CellConstraints.CENTER; } else if (value.equalsIgnoreCase(LEFT)) { alignment = CellConstraints.LEFT; } else if (value.equalsIgnoreCase(RIGHT)) { alignment = CellConstraints.RIGHT; } else if (value.equalsIgnoreCase(TOP)) { alignment = CellConstraints.TOP; } else if (value.equalsIgnoreCase(BOTTOM)) { alignment = CellConstraints.BOTTOM; } else { throw new RuntimeException("Invalid alignment"); } return alignment; } /** * Returns a LayoutConstraintsManager based on an input stream for an xml * file. The root node in the xml file should be called <code>containers</code> and should * adhere to the xml format for this tool. */ public static LayoutConstraintsManager getLayoutConstraintsManager( InputStream stream) { Document dataDocument = null; try { DocumentBuilder documentBuilder = DocumentBuilderFactory .newInstance().newDocumentBuilder(); dataDocument = documentBuilder.parse(stream); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("Unable to create DocumentBuilder", e); } Node root = dataDocument.getDocumentElement(); return getLayoutConstraintsManager(root); } /** * Returns a layout constraints manager given a containers node. This will * enable you to keep a lot of different constraints in a single file or at * least provide a little more flexibility. */ public static LayoutConstraintsManager getLayoutConstraintsManager( Node containersNode) { if (!containersNode.getNodeName().equals("containers")) { throw new RuntimeException("Expected a node named containers"); } LayoutConstraintsManager layoutConstraintsManager = new LayoutConstraintsManager(); Node[] containerNodes = getNodesNamed(containersNode, "container"); for (int index = 0; index < containerNodes.length; index++) { Node containerNode = containerNodes[index]; Map<String, String> containerAttributes = getAttributeMap(containerNode); String containerName = containerAttributes.get("name"); if (containerName == null) { throw new RuntimeException( "Container must have a name attribute"); } String columnSpecs = containerAttributes.get("columnSpecs") != null ? containerAttributes .get("columnSpecs") : ""; String rowSpecs = containerAttributes.get("rowSpecs") != null ? containerAttributes .get("rowSpecs") : ""; final ContainerLayout containerLayout = new ContainerLayout( containerName, columnSpecs, rowSpecs); Node[] cellConstraints = getNodesNamed(containerNode, "cellconstraints"); for (int cIndex = 0; cIndex < cellConstraints.length; cIndex++) { Map<String, String> constraintAttributes = getAttributeMap(cellConstraints[cIndex]); String name = null; CellConstraints.Alignment horizontalAlignment = CellConstraints.DEFAULT; CellConstraints.Alignment verticalAlignment = CellConstraints.DEFAULT; int gridX = 1; int gridY = 1; int gridWidth = 1; int gridHeight = 1; int topInset = 0; int bottomInset = 0; int rightInset = 0; int leftInset = 0; if (constraintAttributes.get("name") == null) { throw new RuntimeException( "cellconstraints attribute name cannot be null for container " + containerName); } name = constraintAttributes.get("name"); if (constraintAttributes.get("horizontalAlignment") != null) { horizontalAlignment = getAlignment(constraintAttributes .get("horizontalAlignment")); } if (constraintAttributes.get("verticalAlignment") != null) { verticalAlignment = getAlignment(constraintAttributes .get("verticalAlignment")); } if (constraintAttributes.get("gridX") != null) { gridX = Integer.parseInt(constraintAttributes.get("gridX")); } if (constraintAttributes.get("gridY") != null) { gridY = Integer.parseInt(constraintAttributes.get("gridY")); } if (constraintAttributes.get("gridWidth") != null) { gridWidth = Integer.parseInt(constraintAttributes .get("gridWidth")); } if (constraintAttributes.get("gridHeight") != null) { gridHeight = Integer.parseInt(constraintAttributes .get("gridHeight")); } if (constraintAttributes.get("topInset") != null) { topInset = Integer.parseInt(constraintAttributes .get("topInset")); } if (constraintAttributes.get("bottomInset") != null) { bottomInset = Integer.parseInt(constraintAttributes .get("bottomInset")); } if (constraintAttributes.get("rightInset") != null) { rightInset = Integer.parseInt(constraintAttributes .get("rightInset")); } if (constraintAttributes.get("leftInset") != null) { leftInset = Integer.parseInt(constraintAttributes .get("leftInset")); } CellConstraints constraints = new CellConstraints(gridX, gridY, gridWidth, gridHeight, horizontalAlignment, verticalAlignment, new Insets(topInset, leftInset, bottomInset, rightInset)); containerLayout.addCellConstraints(name, constraints); } Node[] propertiesNodes = getNodesNamed(containerNode, "properties"); // this is sooooo lame. we now how to construct a fake xml doc // so the parser can read it. i'm starting to think it would have // been easier to just do the whole damn thing by hand. arggg.. String fakeDoc = "<java version=\"1.4.0\" class=\"java.beans.XMLDecoder\">"; fakeDoc += "<void id=\"controller\" property=\"owner\"/>\n"; fakeDoc += "<object idref=\"controller\">"; for (int pIndex = 0; pIndex < propertiesNodes.length; pIndex++) { Node propertiesNode = propertiesNodes[pIndex]; Map<String, String> propertyAttributes = getAttributeMap(propertiesNode); String componentName = propertyAttributes.get("component"); if (componentName == null) { throw new RuntimeException( "propertyset must have an attribute called component"); } Node[] propertyNodes = getNodesNamed(propertiesNode, "property"); for (int propIndex = 0; propIndex < propertyNodes.length; propIndex++) { Node propertyNode = propertyNodes[propIndex]; Map<String, String> voidAttributes = getAttributeMap(propertyNode); String property = voidAttributes.get("name"); if (property == null) { throw new RuntimeException( "property element must have a name"); } fakeDoc += "<void method=\"setProperty\"><string>" + componentName + "</string>"; fakeDoc += "<string>" + property + "</string>"; fakeDoc += createString(propertyNode.getChildNodes()); fakeDoc += "</void>\n"; } } fakeDoc += "</object></java>"; if (propertiesNodes.length > 0) { // Object controller = new Object() // { // public void configureProperty(String componentName, String property, // Object value) // { // containerLayout.setProperty(componentName, property, value); // } // }; XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream( fakeDoc.getBytes())); decoder.setOwner(containerLayout); decoder.readObject(); decoder.close(); } layoutConstraintsManager.addLayout(containerLayout); } return layoutConstraintsManager; } private static Map<String, String> getAttributeMap(Node node) { Map<String, String> attributeMap = new HashMap<String, String>(); NamedNodeMap attributes = node.getAttributes(); if (attributes != null) { for (int index = 0; index < attributes.getLength(); index++) { Node attribute = attributes.item(index); attributeMap.put(attribute.getNodeName(), attribute .getNodeValue()); } } return attributeMap; } private static Node[] getNodesNamed(Node parent, String nodeName) { NodeList children = parent.getChildNodes(); List<Node> childList = new ArrayList<Node>(); for (int i = 0; i < children.getLength(); i++) { if (nodeName.equals(children.item(i).getNodeName())) { childList.add(children.item(i)); } } Node[] result = new Node[childList.size()]; return childList.toArray(result); } public static void main(String[] args) { LayoutConstraintsManager l = LayoutConstraintsManager .getLayoutConstraintsManager(LayoutConstraintsManager.class .getResourceAsStream("editableLayoutConstraints.xml")); /*ContainerLayout cl =*/l.getContainerLayout("mainLayout"); } }