View Javadoc

1   /*
2    * Copyright (c) 2004-2005 by Michael Connor. All Rights Reserved.
3    *
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted provided that the following conditions are met:
6    *
7    *  o Redistributions of source code must retain the above copyright notice,
8    *    this list of conditions and the following disclaimer.
9    *
10   *  o Redistributions in binary form must reproduce the above copyright notice,
11   *    this list of conditions and the following disclaimer in the documentation
12   *    and/or other materials provided with the distribution.
13   *
14   *  o Neither the name of FormLayoutBuilder or Michael Connor nor the names of
15   *    its contributors may be used to endorse or promote products derived
16   *    from this software without specific prior written permission.
17   *
18   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20   * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21   * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
22   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
25   * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27   * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28   * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package org.mlc.swing.layout;
31  
32  import com.jgoodies.forms.layout.CellConstraints;
33  import java.awt.Component;
34  import java.awt.Container;
35  import java.awt.Insets;
36  import java.beans.Statement;
37  import java.beans.XMLDecoder;
38  import java.beans.XMLEncoder;
39  import java.io.ByteArrayInputStream;
40  import java.io.ByteArrayOutputStream;
41  import java.io.InputStream;
42  import java.util.*;
43  import javax.xml.parsers.DocumentBuilder;
44  import javax.xml.parsers.DocumentBuilderFactory;
45  import javax.xml.transform.OutputKeys;
46  import javax.xml.transform.Transformer;
47  import javax.xml.transform.TransformerFactory;
48  import javax.xml.transform.dom.DOMSource;
49  import javax.xml.transform.stream.StreamResult;
50  import org.w3c.dom.Document;
51  import org.w3c.dom.NamedNodeMap;
52  import org.w3c.dom.Node;
53  import org.w3c.dom.NodeList;
54  
55  import javax.swing.*;
56  
57  /***
58   * This class handles the serialization and deserialization of the xml files
59   * that we are using to store the layout constraints.
60   *
61   * <p>In the consuming program, the use of this class might look like:
62  <br><code>
63  InputStream constraints = this.getClass().getResourceAsStream(xmlFile);
64  LayoutConstraintsManager layoutConstraintsManager =
65          LayoutConstraintsManager.getLayoutConstraintsManager(constraints);
66  LayoutManager layout = layoutConstraintsManager.createLayout("panel", this);
67  this.setLayout(layout);
68  </code>
69   *
70   * <p>[I'm sure there are more
71   * elegant ways of handling this (like JAXB) or some other mapping software but
72   * this is simple, it works, and we don't have to package a bunch of other
73   * software or files.]
74   *
75   * @author Michael Connor
76   */
77  public class LayoutConstraintsManager
78  {
79    String defaultColumnSpecs = "right:max(30dlu;pref),3dlu,80dlu,10dlu,right:max(30dlu;pref),3dlu,80dlu,1dlu:grow";
80  
81    String defaultRowSpecs = "pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref,3dlu,pref:grow";
82  
83    static Set<Class> textComponents = new HashSet<Class>();
84  
85    static
86    {
87      textComponents.add(JButton.class);
88      textComponents.add(JCheckBox.class);
89      textComponents.add(JRadioButton.class);
90      textComponents.add(JToggleButton.class);
91      textComponents.add(JLabel.class);
92    }
93  
94    Map<ContainerLayout, Container> containers = new HashMap<ContainerLayout, Container>();
95  
96    List<ContainerLayout> layouts = new ArrayList<ContainerLayout>();
97  
98    /***
99     * This method will create a LayoutConstraintsManager with default JGoodies
100    * row and column specs that are common in applications. The user can then
101    * manipulate these default specs using the LayoutFrame to fine tune the specs
102    * to be whatever they want.
103    */
104   public LayoutConstraintsManager()
105   {
106   }
107 
108   /***
109    * This method will create a LayoutConstraintsManager with the JGoodies specs
110    * provided as default
111    */
112   public LayoutConstraintsManager(String defaultColumnSpecs,
113       String defaultRowSpecs)
114   {
115     this.defaultColumnSpecs = defaultColumnSpecs;
116     this.defaultRowSpecs = defaultRowSpecs;
117   }
118 
119   public List<ContainerLayout> getLayouts()
120   {
121     List<ContainerLayout> list = new ArrayList<ContainerLayout>(layouts.size());
122     list.addAll(layouts);
123     return list;
124   }
125 
126   /***
127    * This method will build a layout from the xml file based on the name and
128    * call setLayout on the container passed in.
129    */
130   public void setLayout(String name, Container container)
131   {
132     ContainerLayout containerLayout = getLayout(name);
133 
134     if (containerLayout == null)
135     {
136       containerLayout = new ContainerLayout(name, defaultColumnSpecs,
137           defaultRowSpecs);
138       layouts.add(containerLayout);
139     }
140 //    else
141 //      containers.remove(containerLayout);
142 
143     container.setLayout(containerLayout);
144     containers.put(containerLayout, container);
145   }
146 
147   /***
148    * This method creates a layout by first trying to look in memory to see if a
149    * layout has been defined with the given name. If a layout exists, it is
150    * returned. Note that when I say in memory that probably means that it was
151    * defined in the xml file. If one doesn't exist, a layout with what I
152    * consider relatively generic constraints will be created and returned. The
153    * reason this method requires the container is because in the case where you
154    * are trying to layout the container visually, I need to be able to get a
155    * handle on the container so I can make calls to add components to it during
156    * interactive layout. This will not be touched at runtime if you are not
157    * doing anything interactively. This method should be seen as a replacement
158    * for LayoutConstraintsManager.setLayout(String name, Container container) as
159    * it's more natural to set the layout on the container yourself.
160    */
161   public ContainerLayout createLayout(String name, Container container)
162   {
163     ContainerLayout containerLayout = getLayout(name);
164 
165     if (containerLayout == null)
166     {
167       containerLayout = new ContainerLayout(name, defaultColumnSpecs,
168           defaultRowSpecs);
169       layouts.add(containerLayout);
170     }
171 
172     containers.put(containerLayout, container);
173     return containerLayout;
174   }
175 
176   public Container getContainer(ContainerLayout layout)
177   {
178     return containers.get(layout);
179   }
180 
181   private ContainerLayout getLayout(String name)
182   {
183     for (int i = 0; i < layouts.size(); i++)
184       if (layouts.get(i).getName().equals(name))
185         return layouts.get(i);
186 
187     return null;
188   }
189 
190 // KBR NYI
191 //  public List<String> getContainerNames()
192 //  {
193 //    List<String> names = new ArrayList<String>();
194 //
195 //    return names;
196 //  }
197 
198   public ContainerLayout getContainerLayout(String containerName)
199   {
200     ContainerLayout layout = getLayout(containerName);
201     return layout;
202   }
203 
204   public void removeLayout(ContainerLayout containerLayout)
205   {
206     layouts.remove(containerLayout);
207   }
208 
209   public void addLayout(ContainerLayout containerLayout)
210   {
211     layouts.add(containerLayout);
212   }
213 
214   /***
215     Get an XML representation of the FormLayout constraints for all containers
216     in this manager.
217   */
218   public String getXML()
219   {
220     StringBuffer xml = new StringBuffer(
221         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n");
222     xml.append("<containers>\n");
223 
224     for (int index = 0; index < layouts.size(); index++)
225     {
226       ContainerLayout layout = layouts.get(index);
227       LinkedHashMap<String, CellConstraints> constraintMap = layout
228           .getCellConstraints();
229 
230       xml.append("    <container name=\"" + layout.getName() + "\"\n");
231       xml.append("               columnSpecs=\""
232           + layout.getColumnSpecsString() + "\"\n");
233       xml.append("               rowSpecs=\"" + layout.getRowSpecsString()
234           + "\">\n");
235 
236       for (Iterator componentNames = constraintMap.keySet().iterator(); componentNames
237           .hasNext();)
238       {
239         String componentName = (String) componentNames.next();
240         CellConstraints constraints = constraintMap.get(componentName);
241         xml.append("        <cellconstraints ");
242         xml.append("name=\"" + componentName + "\" ");
243         xml.append("gridX=\"" + constraints.gridX + "\" ");
244         xml.append("gridY=\"" + constraints.gridY + "\" ");
245         xml.append("gridWidth=\"" + constraints.gridWidth + "\" ");
246         xml.append("gridHeight=\"" + constraints.gridHeight + "\" ");
247         xml.append("horizontalAlignment=\"" + getAlignment(constraints.hAlign)
248             + "\" ");
249         xml.append("verticalAlignment=\"" + getAlignment(constraints.vAlign)
250             + "\" ");
251         xml.append("topInset=\"" + constraints.insets.top + "\" ");
252         xml.append("bottomInset=\"" + constraints.insets.bottom + "\" ");
253         xml.append("rightInset=\"" + constraints.insets.right + "\" ");
254         xml.append("leftInset=\"" + constraints.insets.left + "\"/>\n");
255       }
256 
257       for (Iterator componentNames = constraintMap.keySet().iterator(); componentNames
258           .hasNext();)
259       {
260         String componentName = (String) componentNames.next();
261         Component component = layout.getComponentByName(componentName);
262 
263         if (component != null)
264         {
265           Map<String, Object> customProperties = layout
266               .getCustomProperties(componentName);
267 
268           boolean hasProperties = false;
269           boolean isTextComponent = isTextComponent(component);
270           // we need to look through these and see if we have
271           // any valid properties. the text props don't count
272           // for controls like JLabel, JButton, etc. because
273           // we'll put those in the constructor.
274           for (String propertyName : customProperties.keySet())
275           {
276             if ((!isTextComponent) || (!propertyName.equals("text")))
277             {
278               hasProperties = true;
279               break;
280             }
281           }
282 
283           if (hasProperties)
284           {
285             xml.append("\n        <properties component=\"" + componentName
286                 + "\">\n");
287             for (String propertyName : customProperties.keySet())
288             {
289 
290               if (isTextComponent && propertyName.equals("text"))
291                 break;
292 
293               ByteArrayOutputStream stream = new ByteArrayOutputStream();
294               XMLEncoder xmlEncoder = new XMLEncoder(stream);
295               xmlEncoder.setOwner(component);
296 
297               String methodName = "set"
298                   + propertyName.substring(0, 1).toUpperCase()
299                   + (propertyName.length() > 1 ? propertyName.substring(1) : "");
300               xmlEncoder.writeStatement(new Statement(xmlEncoder, methodName,
301                   new Object[] { customProperties.get(propertyName) }));
302               xmlEncoder.close();
303 
304               String propertyXml = new String(stream.toByteArray());
305               int voidStart = propertyXml.indexOf("<void");
306               int voidEnd = propertyXml.indexOf(">", voidStart);
307               int end = propertyXml.lastIndexOf("</void>");
308               String xmlWithoutDec = propertyXml.substring(voidEnd + 1, end).trim();
309               String indented = "          "
310                   + xmlWithoutDec.replaceAll("\n", "\n        ");
311               xml.append("         <property name=\"" + propertyName + "\">");
312               xml.append(indented);
313               xml.append("</property>\n");
314             }
315 
316             xml.append("        </properties>\n");
317           }
318         }
319       }
320 
321       xml.append("    </container>\n");
322     }
323 
324     xml.append("</containers>\n");
325     return xml.toString();
326   }
327 
328   public static boolean isTextComponent(Component component)
329   {
330     for (Class clazz : textComponents)
331     {
332       if (clazz.isAssignableFrom(component.getClass()))
333         return true;
334     }
335     return false;
336   }
337 
338   private static String createString(NodeList childNodes)
339   {
340 
341     try
342     {
343       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
344       TransformerFactory tFactory = TransformerFactory.newInstance();
345       Transformer transformer = tFactory.newTransformer();
346       transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
347 
348       for (int i = 0; i < childNodes.getLength(); i++)
349       {
350         Node child = childNodes.item(i);
351         DOMSource source = new DOMSource(child);
352         StreamResult result = new StreamResult(byteStream);
353         transformer.transform(source, result);
354       }
355       return new String(byteStream.toByteArray());
356     }
357     catch (Exception e)
358     {
359       e.printStackTrace();
360       throw new RuntimeException("unexpected error", e);
361     }
362   }
363 
364   public static final String DEFAULT = "default";
365 
366   public static final String FILL = "fill";
367 
368   public static final String CENTER = "center";
369 
370   public static final String LEFT = "left";
371 
372   public static final String RIGHT = "right";
373 
374   public static final String TOP = "top";
375 
376   public static final String BOTTOM = "bottom";
377 
378   /***
379    * Translates an alignment value to a string.
380    */
381   public static String getAlignment(CellConstraints.Alignment alignment)
382   {
383     String value = null;
384 
385     if (alignment == CellConstraints.DEFAULT)
386       value = DEFAULT;
387     else if (alignment == CellConstraints.FILL)
388       value = FILL;
389     else if (alignment == CellConstraints.CENTER)
390       value = CENTER;
391     else if (alignment == CellConstraints.LEFT)
392       value = LEFT;
393     else if (alignment == CellConstraints.RIGHT)
394       value = RIGHT;
395     else if (alignment == CellConstraints.TOP)
396       value = TOP;
397     else if (alignment == CellConstraints.BOTTOM)
398       value = BOTTOM;
399 
400     if (value == null)
401       throw new RuntimeException("Unknown alignment type");
402     else
403       return value;
404   }
405 
406   /***
407    * Translates a string to an alignment value.
408    */
409   public static CellConstraints.Alignment getAlignment(String value)
410   {
411     CellConstraints.Alignment alignment = null;
412 
413     if (value.equalsIgnoreCase(DEFAULT))
414       alignment = CellConstraints.DEFAULT;
415     else if (value.equalsIgnoreCase(FILL))
416       alignment = CellConstraints.FILL;
417     else if (value.equalsIgnoreCase(CENTER))
418       alignment = CellConstraints.CENTER;
419     else if (value.equalsIgnoreCase(LEFT))
420       alignment = CellConstraints.LEFT;
421     else if (value.equalsIgnoreCase(RIGHT))
422       alignment = CellConstraints.RIGHT;
423     else if (value.equalsIgnoreCase(TOP))
424       alignment = CellConstraints.TOP;
425     else if (value.equalsIgnoreCase(BOTTOM))
426       alignment = CellConstraints.BOTTOM;
427     else
428       throw new RuntimeException("Invalid alignment");
429 
430     return alignment;
431   }
432 
433   /***
434    * Returns a LayoutConstraintsManager based on an input stream for an xml
435    * file. The root node in the xml file should be called <code>containers</code> and should
436    * adhere to the xml format for this tool.
437    */
438   public static LayoutConstraintsManager getLayoutConstraintsManager(
439       InputStream stream)
440   {
441     Document dataDocument = null;
442 
443     try
444     {
445       DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance()
446           .newDocumentBuilder();
447       dataDocument = documentBuilder.parse(stream);
448     }
449     catch (Exception e)
450     {
451       e.printStackTrace();
452       throw new RuntimeException("Unable to create DocumentBuilder", e);
453     }
454 
455     Node root = dataDocument.getDocumentElement();
456     return getLayoutConstraintsManager(root);
457   }
458 
459   /***
460    * Returns a layout constraints manager given a containers node. This will
461    * enable you to keep a lot of different constraints in a single file or at
462    * least provide a little more flexibility.
463    */
464   public static LayoutConstraintsManager getLayoutConstraintsManager(
465       Node containersNode)
466   {
467 
468     if (!containersNode.getNodeName().equals("containers"))
469       throw new RuntimeException("Expected a node named containers");
470 
471     LayoutConstraintsManager layoutConstraintsManager = new LayoutConstraintsManager();
472     Node[] containerNodes = getNodesNamed(containersNode, "container");
473 
474     for (int index = 0; index < containerNodes.length; index++)
475     {
476       Node containerNode = containerNodes[index];
477 
478       Map<String, String> containerAttributes = getAttributeMap(containerNode);
479       String containerName = containerAttributes.get("name");
480       if (containerName == null)
481         throw new RuntimeException("Container must have a name attribute");
482       String columnSpecs = containerAttributes.get("columnSpecs") != null ? containerAttributes
483           .get("columnSpecs")
484           : "";
485       String rowSpecs = containerAttributes.get("rowSpecs") != null ? containerAttributes
486           .get("rowSpecs")
487           : "";
488 
489       final ContainerLayout containerLayout = new ContainerLayout(
490           containerName, columnSpecs, rowSpecs);
491 
492       Node[] cellConstraints = getNodesNamed(containerNode, "cellconstraints");
493       for (int cIndex = 0; cIndex < cellConstraints.length; cIndex++)
494       {
495         Map<String, String> constraintAttributes = getAttributeMap(cellConstraints[cIndex]);
496 
497         String name = null;
498         CellConstraints.Alignment horizontalAlignment = CellConstraints.DEFAULT;
499         CellConstraints.Alignment verticalAlignment = CellConstraints.DEFAULT;
500         int gridX = 1;
501         int gridY = 1;
502         int gridWidth = 1;
503         int gridHeight = 1;
504         int topInset = 0;
505         int bottomInset = 0;
506         int rightInset = 0;
507         int leftInset = 0;
508 
509         if (constraintAttributes.get("name") == null)
510           throw new RuntimeException(
511               "cellconstraints attribute name cannot be null for container "
512                   + containerName);
513         name = constraintAttributes.get("name");
514         if (constraintAttributes.get("horizontalAlignment") != null)
515           horizontalAlignment = getAlignment(constraintAttributes
516               .get("horizontalAlignment"));
517         if (constraintAttributes.get("verticalAlignment") != null)
518           verticalAlignment = getAlignment(constraintAttributes
519               .get("verticalAlignment"));
520         if (constraintAttributes.get("gridX") != null)
521           gridX = Integer.parseInt(constraintAttributes.get("gridX"));
522         if (constraintAttributes.get("gridY") != null)
523           gridY = Integer.parseInt(constraintAttributes.get("gridY"));
524         if (constraintAttributes.get("gridWidth") != null)
525           gridWidth = Integer.parseInt(constraintAttributes.get("gridWidth"));
526         if (constraintAttributes.get("gridHeight") != null)
527           gridHeight = Integer.parseInt(constraintAttributes.get("gridHeight"));
528         if (constraintAttributes.get("topInset") != null)
529           topInset = Integer.parseInt(constraintAttributes.get("topInset"));
530         if (constraintAttributes.get("bottomInset") != null)
531           bottomInset = Integer.parseInt(constraintAttributes
532               .get("bottomInset"));
533         if (constraintAttributes.get("rightInset") != null)
534           rightInset = Integer.parseInt(constraintAttributes.get("rightInset"));
535         if (constraintAttributes.get("leftInset") != null)
536           leftInset = Integer.parseInt(constraintAttributes.get("leftInset"));
537 
538         CellConstraints constraints = new CellConstraints(gridX, gridY,
539             gridWidth, gridHeight, horizontalAlignment, verticalAlignment,
540             new Insets(topInset, leftInset, bottomInset, rightInset));
541 
542         containerLayout.addCellConstraints(name, constraints);
543       }
544 
545       Node[] propertiesNodes = getNodesNamed(containerNode, "properties");
546 
547       // this is sooooo lame. we now how to construct a fake xml doc
548       // so the parser can read it. i'm starting to think it would have
549       // been easier to just do the whole damn thing by hand. arggg..
550       StringBuffer fakeDoc = new StringBuffer("<java version=\"1.4.0\" class=\"java.beans.XMLDecoder\">");
551       fakeDoc.append("<void id=\"controller\" property=\"owner\"/>\n");
552       fakeDoc.append("<object idref=\"controller\">");
553 
554       for (int pIndex = 0; pIndex < propertiesNodes.length; pIndex++)
555       {
556         Node propertiesNode = propertiesNodes[pIndex];
557         Map<String, String> propertyAttributes = getAttributeMap(propertiesNode);
558         String componentName = propertyAttributes.get("component");
559         if (componentName == null)
560           throw new RuntimeException(
561               "propertyset must have an attribute called component");
562 
563         Node[] propertyNodes = getNodesNamed(propertiesNode, "property");
564         for (int propIndex = 0; propIndex < propertyNodes.length; propIndex++)
565         {
566           Node propertyNode = propertyNodes[propIndex];
567           Map<String, String> voidAttributes = getAttributeMap(propertyNode);
568           String property = voidAttributes.get("name");
569           if (property == null)
570             throw new RuntimeException("property element must have a name");
571           fakeDoc.append("<void method=\"setProperty\"><string>" + componentName
572               + "</string>");
573           fakeDoc.append("<string>" + property + "</string>");
574           fakeDoc.append(createString(propertyNode.getChildNodes()));
575           fakeDoc.append("</void>\n");
576 
577         }
578       }
579 
580       fakeDoc.append("</object></java>");
581 
582       if (propertiesNodes.length > 0)
583       {
584 
585         Object controller = new Object()
586         {
587           public void configureProperty(String componentName, String property,
588               Object value)
589           {
590             containerLayout.setProperty(componentName, property, value);
591           }
592         };
593 
594         XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream(fakeDoc
595             .toString().getBytes()));
596         decoder.setOwner(containerLayout);
597         decoder.readObject();
598         decoder.close();
599       }
600 
601       layoutConstraintsManager.addLayout(containerLayout);
602     }
603 
604     return layoutConstraintsManager;
605   }
606 
607   private static Map<String, String> getAttributeMap(Node node)
608   {
609 
610     Map<String, String> attributeMap = new HashMap<String, String>();
611 
612     NamedNodeMap attributes = node.getAttributes();
613     if (attributes != null)
614     {
615       for (int index = 0; index < attributes.getLength(); index++)
616       {
617         Node attribute = attributes.item(index);
618         attributeMap.put(attribute.getNodeName(), attribute.getNodeValue());
619       }
620     }
621 
622     return attributeMap;
623   }
624 
625   private static Node[] getNodesNamed(Node parent, String nodeName)
626   {
627     NodeList children = parent.getChildNodes();
628     List<Node> childList = new ArrayList<Node>();
629     for (int i = 0; i < children.getLength(); i++)
630     {
631       if (nodeName.equals(children.item(i).getNodeName()))
632         childList.add(children.item(i));
633     }
634     Node[] result = new Node[childList.size()];
635     return (Node[]) childList.toArray(result);
636   }
637 
638   public static void main(String[] args)
639   {
640     LayoutConstraintsManager l = LayoutConstraintsManager
641         .getLayoutConstraintsManager(LayoutConstraintsManager.class
642             .getResourceAsStream("editableLayoutConstraints.xml"));
643     ContainerLayout cl = l.getContainerLayout("mainLayout");
644   }
645 
646 }