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